Skip to content

perf: incremental memory optimization across Maka - #5153

Merged
M4n5ter merged 85 commits into
mainfrom
perf/desktop-idle-memory
Sep 11, 2026
Merged

perf: incremental memory optimization across Maka#5153
M4n5ter merged 85 commits into
mainfrom
perf/desktop-idle-memory

Conversation

@M4n5ter

@M4n5ter M4n5ter commented Sep 10, 2026

Copy link
Copy Markdown
Member
English

Summary

Reduce unused component residency, obsolete object retention and redundant allocations across Desktop, CLI, Runtime, Runtime Host and integrations. Preserve active drafts, attachments, complete durable content and running-work ownership.

Memory growth priority

These classifications describe the behavior before the corresponding fixes. “Cumulative” means obsolete objects remain owned as operations repeat, not that every optimization fixes a leak.

Growth class Items Retained data / boundary
Cumulative payload/resource retention 03 — attachment previews Repeated unmounts leave Blob URLs and preview payloads.
Cumulative payload/resource retention 06 — graph epochs Each handover leaves an old complete graph snapshot.
Cumulative payload/resource retention 07 — transcript subscriptions Open connections retain buffers from ended subscriptions.
Cumulative payload/resource retention 10 — subscription preparation Refresh/recovery retains obsolete replicas and preparation graphs.
Cumulative payload/resource retention 13 — native WebRTC Repeated connection closure leaves tasks and native resources.
Cumulative payload/resource retention 16 — code-language cache New unsupported labels accumulate cache entries that can retain complete messages.
Cumulative payload/resource retention 17 — CLI event waits A long-running command retains consumed wait records and event payloads.
Cumulative metadata retention 08 — browser release fences Released-session keys accumulate.
Cumulative metadata retention 27 — Computer Use cleanup Retired session generation/observer state accumulates.
Cumulative metadata retention 29 — Goal token counts Archived/removed-session totals remain cached.
Cumulative metadata retention 40 — plugin isolation labels Replaced/unloaded versions leave interned Symbols.
Cumulative metadata retention 60 — interaction hydration fences Historical Session keys accumulate in AppShell.
Conditional accumulation 05 — display callbacks; 30 — MCP blocked sends Accumulate while background rAF is suspended or transport backpressure persists; resuming painting/draining can release the old retention.
Growth with required history 04 — message replay; 09 — admission history; 18 — undo history Smaller records/shared text reduce growth cost; history is still retained and total memory is not constant.
Cumulative product trigger unconfirmed 11 — Zod parsing Ordinary parsing retains prior state; repeated exception-stack accumulation has no confirmed ordinary product trigger.
Bounded residency / temporary allocation Remaining items Not established as cumulative leaks. For example, 15's old timers expire within five minutes; 28's upload staging is capped at 128 MiB.

The five metadata items are structurally cumulative but generally smaller than retained message bodies, buffers or native resources.

Unresolved plugin module lifetime

  • Release executable ESM generations after plugin reload/uninstall.
Status Trigger Effect Required boundary
Unfixed; outside the 64 optimizations Fresh native ESM imports on reload/replacement or failed-package retry A synthetic 2 MiB-per-generation package retains all 25 module payloads after 24 reloads, uninstall, close and GC on Node 24. Entry activation state and Contexts are released. An unloadable execution owner or a collectible executable-artifact contract. Reusing import URLs changes fresh-module-state semantics.

The 64 changes below distinguish lifetime fixes from temporary-allocation reductions. Figures apply only to the specified inputs and paths; they are not additive whole-application or RSS savings.

Optimization Before After Effect
01. WorkHub initialization Enabling WorkHub preloaded a second application view. Create on first use; show cold summons after renderer readiness, retaining drafts and attachments afterward. Avoid the unused view's renderer/DOM memory.
02. Bot SDK loading Bot modules loaded Feishu, WeCom and Slack SDKs eagerly. Load each SDK when its connection or credential probe needs it. Avoid SDK residency for unused integrations.
03. Attachment previews Unmount or late async results could leave preview URLs and staged data owned. Revoke owned Blob URLs on unmount and reject late preview/staging work. Release orphaned previews without discarding live drafts.
04. Message replay records Historical replay identities retained full submitted and edited bodies. Keep content digests for identity; preserve complete results and replay rules. Reduce historical body retention without evicting replay entries.
05. Stream display callbacks Completed rAF/timeout races left the losing callback pending. Cancel the other callback when either display trigger fires. Prevent stale callbacks accumulating when background painting pauses.
06. Agent graph epochs Historical epochs retained complete reconciliation snapshots. Release snapshots after handover and active readers finish; retain repair metadata. Stop old graph snapshots accumulating.
07. Transcript subscriptions Connection-lifetime promise waits retained released snapshot buffers. Use removable close listeners and detach them when the subscription ends. Release transcript buffers while the connection stays open.
08. Browser release fences A session-to-epoch map retained every released session key. Keep fences only for pending connection attempts. Bound fencing metadata by pending work, preserving deletion races.
09. Root Turn admission history Historical admission identities retained full request bodies. Store digests and owned identity metadata; keep active/recovery results complete. Reduce historical request-body retention without weakening replay checks.
10. Subscription preparation Activated subscriptions retained failure promises, resolvers and preparation graphs. Clear preparation state after activation and catch-up drain. Allow old replicas, handles and snapshots to collect.
11. Zod recursive parsing Schemas retained previous parse contexts; synchronous throws could leave allocation state. Remove historical context references and clear allocation state on throws. Release prior parse graphs while preserving cycles and shared aliases.
12. WeChat SSE reads The next idle wait retained the last raw message, event and byte chunk. Release processed read values before waiting again. Remove last-message idle retention, not a growing history buffer.
13. Native WebRTC lifetime Connection termination could leave negotiation/data-channel tasks and peer resources alive. Tie tasks and peer closure to one connection lifetime guard. Release native connection resources on cancellation, close or drop.
14. Local-message retries Five-second retry closures retained the full delivery scope and body. Capture only the service and message key; keep pending content in SQLite. Avoid redundant body retention during retry waits.
15. Peer Mesh timers Early reconciliation wake-ups left obsolete five-minute timers/listeners. Clean each wait's timer and abort listener on settlement. Avoid accumulating inactive waits.
16. Code-language cache Unsupported language labels were cached as null and could retain large message strings. Cache only supported language definitions. Stop arbitrary labels retaining old messages; keep plain-text fallback.
17. CLI event waits Run-lifetime promise races retained consumed event/poll wait records. Detach failure waiters after each event read or graph poll. Release consumed records during long-running CLI sessions.
18. CLI undo snapshots Undo snapshots repeatedly deep-copied large pasted text. Copy mutable containers while sharing immutable strings. Reduce undo memory without removing history steps.
19. Short-text truncation Truncation expanded the complete input into a code-point array. Collect only the budgeted prefix plus a truncation-detection character. Bound temporary character arrays; normalization stays unchanged.
20. Diagnostic truncation Small log outputs could allocate full-input character arrays and UTF-8 buffers. Collect/encode only budget-relevant text and markers. Reduce transient allocation while preserving redaction and byte limits.
21. Peer receive parser Authentication remainders, consumed chunks and empty views stayed referenced. Release consumed parser inputs before the next wait. Drop processed buffer residency; preserve partial frames and replay data.
22. Reasoning bodies Collapsed, never-opened reasoning mounted full Markdown/code subtrees. Mount on first expansion; retain nodes and state on later collapse. Reduce initial DOM/heap; full-body page search/accessibility traversal requires first expansion, available by keyboard.
23. Search result snippets Truncated titles/snippets could keep complete provider-text backing strings. Copy only prefixes that actually truncate. Short results no longer retain oversized source strings.
24. Update attestation loading Reading startup channel metadata loaded Sigstore/TUF dependencies. Load them only for bundle parsing or signature verification. Avoid verifier dependency residency on metadata-only startup.
25. Customized request cancellation Cancellation depended on a temporary Request's forwarded signal lifetime. Forward the caller's effective AbortSignal directly. Keep cancellation effective so abandoned requests can stop.
26. JSON request overlays Overlay/finalizer handling cloned and reread an already buffered request body. Decode the first ArrayBuffer instead. Remove a redundant whole-body read/copy.
27. Computer Use cleanup Cleanup created generation/observer state even for unused sessions and kept retired fences. Do not notify observers for locally unknown sessions; keep generation fences only through the last live queue tail. Avoid historical cleanup-state growth.
28. Removed-session uploads Missing-session rejections could leave unusable staged upload buffers. Release the requesting connection/Host Epoch's staging on that rejection. Free rejected upload data promptly; preserve the error and ownership boundary.
29. Retired Goal token counts Archived/removed sessions retained token totals; late reads could repopulate them. Clear retired totals and recheck the control lease before async writes. Stop obsolete token metadata accumulating or overwriting a new Goal.
30. MCP blocked sends Backpressured send observers retained already-settled request object graphs. Isolate cancellation observers and release reject references on settlement. Release finished request graphs without dropping queued bytes.
31. Skill inventory snapshots Each cached turn owned another identical inventory snapshot. Share frozen snapshots only when revisions and full contents match. Reduce duplicate inventory storage; keep fresh reads and the 100-turn history.
32. System-prompt text Completed turns retained separate copies of identical assembled base prompts. Share equal completed prompt strings; keep reads and revision objects separate. Reduce duplicate prompt backing; dynamic per-step additions stay fresh.
33. Bounded draft history A 120,000-character draft slice could retain its oversized original input. Copy only the retained window when truncation occurs. Release oversized backing strings without changing the 32-entry history.
34. Responses WebSocket history Continuation reconstruction deep-copied private prior input/output and the new delta. Build a new array reusing privately owned history values. Remove repeated history deep copies; caller-facing defensive copies remain.
35. Completed stream displays Capped final display slices could retain oversized full-output strings. Copy the final bounded head/tail. Release full-output backing while preserving display and durable text.
36. Stream reseed buffers Bounded display/redaction-recovery slices could retain oversized raw deltas. Copy the final bounded strings for oversized deltas. Release oversized backing without changing recovery contents.
37. Archive pages Small returned pages could retain complete archive backing strings. Clone once at the final ArchiveRead return boundary. Detach bounded pages from full archives; pagination and authority stay unchanged.
38. Tool settlement batches Private raw tool outcomes stayed alive after settlement during continuation. Clear the batch after settlements and ordered Plan/Yield handling. Release completed outcomes sooner; durable events retain complete results.
39. Sent-prompt history Normalized short sent text could retain oversized input backing. Copy only when wire normalization shortens the text. Release trimmed backing while preserving send/recall and rejected drafts.
40. Plugin isolation labels Strong interning retained labels from replaced or unloaded plugin versions. Weakly intern by Symbol lifetime with stale-finalizer protection. Reclaim unused label metadata after GC; ESM module retention is unchanged.
41. Memory-extraction splits All candidate prefix/suffix arrays were materialized before trying the first split. Materialize candidates in order and stop after the first usable split. Avoid quadratic eager reference copying; worst-case search work is unchanged.
42. Memory-localization previews Short previews expanded entire assistant replies into code-point arrays. Collect only the required 2,000-code-point prefix. A 256 KiB ASCII reply uses 2,000 array entries instead of 262,144.
43. Session-catalog previews A short catalog preview allocated a full-message character array. Retain at most 96 code points and inspect the next character for truncation. Bound preview arrays independently of full-message size.
44. Agent graph handoff prefixes Budgeted handoff text expanded the entire reply into a character array. Collect only the existing byte-budget prefix. A 256 KiB ASCII reply's array shrinks from 262,144 to 16,384 entries.
45. Child-agent result envelopes Bounded JSON output expanded the full child reply into code points. Collect only characters that could fit the output budget. A 256 KiB ASCII reply's array shrinks from 262,144 to 32,768 entries.
46. Workspace instructions Prefix selection and truncation detection both expanded complete instructions. Collect at most 6,000 code points and inspect one extra character. Replace two full character arrays with one bounded prefix.
47. PTY model-output tails Tail truncation expanded full scrollback and built per-character concatenations. Collect fitting code points directly from the end. Remove full-scrollback temporary arrays and concatenation chains.
48. OAuth error bodies Reading an error response clone left the discarded original branch buffered. Read unsuccessful response bodies directly once. Avoid an extra error-body buffer during refresh waits; successful SSE stays unread.
49. Stable file-read buffers Small admitted files allocated the full configured maximum plus a sentinel byte. Allocate the admitted file size plus one growth-detection byte. An 881-byte file requests 882 bytes instead of 262,145; identity checks remain.
50. Consumed framed buffers A zero-length tail view kept the final receive buffer's backing store alive. Use an empty Buffer after the last complete frame. Release final-buffer residency per open connection; partial tails remain owned.
51. Complete IPC frame copies The decoder concatenated an empty prefix with its already-owned input copy. Use that owned copy directly when no partial prefix exists. Remove one redundant full-chunk copy for complete frames.
52. Composer history initialization Each render read and parsed persisted history even after initialization. Initialize history once per mount. Remove repeated storage reads, JSON parsing and discarded history strings.
53. Peer handshake/read lifetime The pump retained a consumed handshake remainder or last chunk while waiting. Transfer one-use remainder ownership and release consumed chunks before waits. Release consumed per-connection buffers; backpressure and recovery stay intact.
54. Live PTY chunking Each chunk became a full code-point array followed by sliced arrays. Walk UTF-16 boundaries directly while preserving 4,096-code-point pieces. Remove full character arrays and their slices from live output.
55. Base64 byte counts Already validated or locally encoded Base64 was decoded just to measure size. Count decoded length without creating a Buffer. Remove counting-only decodes; a 512 KiB page's decode allocation falls from 1.5 MiB to 512 KiB.
56. Research artifact windows Reading a character window expanded the full document into an array. Count the full document but collect only requested code points. Remove the full-document array; keep only the 32k-default/64k-max window.
57. Edit line positions Line positions were counted by splitting entire prefixes and edited spans. Count newlines without producing line arrays. Remove temporary position-counting arrays; edit matching and output stay unchanged.
58. Localized edit diffs A small diff split both full files into line arrays. Collect only local windows under the existing 800-line/32 KiB caps. Remove two full-file arrays; preserve hunk context and formatting.
59. Partial file reads Selecting a few lines split the complete file into a line array. Share selected-line scanning across Workspace and filesystem Worker. Remove full-file line arrays; full input reads still occur.
60. Interaction hydration fences AppShell permanently retained historical Session→epoch keys. Use identities owned only by pending reads and their cleanup. Remove historical fence-key growth while preserving late-response protection.
61. Tool-output line windows Head/tail previews split complete tool output before limiting lines. Locate boundaries and collect only retained lines. Remove whole-output line arrays; line/byte budgets and markers stay unchanged.
62. Huge-line previews A huge single line was fully UTF-8 encoded before clipping. Select a budget-sized UTF-16 window before encoding. A 1,000,000-byte ASCII line encodes 51,201 bytes; boundary behavior is preserved.
63. WebFetch model prefixes A full fetched body was encoded to produce a capped model preview. Encode only the prefix needed by the 50 KiB inclusive-marker budget. A 4 MiB ASCII body encodes 51,132 bytes; full fetch/read/parsing remain.
64. Runtime Resource cloning Each update was cloned, then its result was cloned again for bounding. Clone the update once and bound its cloned result in place. Reduce result deep copies from two to one; isolation, responses and revisions stay unchanged.

Item 22 measurement and trade-off

A fixed synthetic transcript rendered through the real TurnView (20 reasoning blocks × 180 code lines, 20 collapsed tool results, one final answer; identical 376,472-byte viewmodel JSON) was compared in separate Chromium 151 processes. Post-GC JS heap: 10,839,440 → 4,895,284 bytes (5.67 MiB less); DOM elements: 5,020 → 679. This measures component residency, not OS RSS or a representative workload distribution. Text remains retained. Browser page search found a deep marker before expansion in the old implementation but requires first expansion now; opening the first block restored 217 body descendants and marker search. Keyboard expansion does not remove this discovery cost. No real screen-reader test was performed. Keep first-expansion mounting; no unmeasured length threshold is introduced.

Verification

Round-two follow-up 2bb009541: added targeted cancellation, Goal lease/baseline, Zod ESM/CJS recursive identity, and WebRTC teardown coverage. Two affected TypeScript workspaces build; 18 targeted JS tests and all 40 Rust tests pass; lint/format, ASF headers and Rust Clippy pass. Fault-injection checks fail when request signal forwarding, the Goal lease gate, or Zod handoff restoration is removed. These are contract tests, not allocation-size benchmarks; no production implementation changed in this follow-up.

Review follow-up 4bbe8fd3d: five affected TypeScript projects compile; 640 tests pass (Graph/Bots 115, WorkHub 24, Computer Use 57, Peer 14, UI 430). The Graph and SDK regressions also fail against pre-follow-up source. No new allocation-probe files were added.

Original 64-change verification, before these follow-ups:

  • Linux / Node 24: lint, format, build, workspace typechecks, Knip and repository policy checks pass; standard workspace suites: 9,759 passed.
  • Runtime Host: default-concurrency run has 6 failures; full suite at concurrency 2: 1,845 passed, 12 skipped, 0 failed. Default-concurrency stability is not established.
  • Desktop E2E: 37 passed; browser smoke: 17 passed; alignment: clean; Storybook: 332 stories / 359 theme renders passed.
  • Rust format, Clippy, licenses, notices and tests; release contracts; Linux sandbox; Docker egress proxy/toolchain; published-CLI state-root forward-roll: pass.
  • npm 11.19 install, CLI candidate packaging and offline-installed CLI smoke: pass.

UI command output:

37 passed (10.1m)
Storybook render smoke passed (332 stories, 359 theme renders).

Not covered: full Windows/macOS packaged-app suites, live Feishu/WeCom connections or all-platform native scenarios. Allocation-specific regression tests are not included in this PR.

AI use

  • No generative tool made a substantive contribution
  • Generative tooling made a substantive contribution

Tool(s) and scope: OpenAI Codex — implementation, verification and PR description. Affected commits include Generated-by: OpenAI Codex.

Checklist

  • Tests cover the change and fail without it
  • Lint, format, typecheck and the affected suites pass locally

Does this PR entail a change in behavior?

  • Yes — described under Summary above
  • No
中文

概要

减少 Desktop、CLI、Runtime、Runtime Host 与集成模块中未使用组件的驻留、过期对象保留及重复分配。保留活动草稿、附件、完整持久化内容及运行中任务的所有权。

内存增长优先级

以下分类描述各项修复前的行为。“持续累积”指旧对象随重复操作继续被持有,不代表每项优化都是泄漏修复。

增长类型 对应项 累积对象/边界
正文/资源持续累积 03 — 附件预览 反复卸载后遗留 Blob URL 与预览载荷。
正文/资源持续累积 06 — 图 epoch 每次交接留下旧图完整快照。
正文/资源持续累积 07 — Transcript 订阅 长连接保留已结束订阅的 Buffer。
正文/资源持续累积 10 — 订阅准备状态 刷新/恢复后保留旧副本与准备对象图。
正文/资源持续累积 13 — 原生 WebRTC 反复关闭连接后遗留任务与原生资源。
正文/资源持续累积 16 — 代码语言缓存 新的未知标签不断进入缓存,并可能保留完整消息。
正文/资源持续累积 17 — CLI 事件等待 长任务持续保留已消费等待记录与事件载荷。
元数据持续累积 08 — 浏览器释放防护 已释放会话键不断累积。
元数据持续累积 27 — Computer Use 清理 退役会话的 generation/观察状态累积。
元数据持续累积 29 — Goal token 计数 归档/删除会话的总量仍被缓存。
元数据持续累积 40 — 插件隔离标签 已替换/卸载版本留下强引用 Symbol。
元数据持续累积 60 — 交互回填防护 AppShell 持续积累历史 Session 键。
条件性累积 05 — 显示回调;30 — MCP 发送背压 后台 rAF 暂停或传输持续背压时累积;恢复绘制/排空发送后,原实现也可能释放。
随必要历史增长 04 — 消息重放;09 — 接纳历史;18 — 撤销历史 缩小记录/共享正文降低增长成本;历史仍保留,总内存并非恒定。
持续累积的产品触发未确认 11 — Zod 解析 普通解析保留旧状态;异常栈重复累积尚未确认日常产品触发。
有界驻留/临时分配 其余项 未确认为持续泄漏。例如 15 的旧定时器五分钟内过期,28 的上传暂存上限为 128 MiB。

五项元数据问题虽会持续累积,但通常不能与完整消息、Buffer 或原生资源按同等量级看待。

尚未解决的插件模块生命周期

  • 插件重载/卸载后释放可执行 ESM 代。
状态 触发条件 效果 所需边界
未修复,不计入 64 项优化 重载/替换或失败插件重试时,以新 URL 原生导入 ESM 每代 2 MiB 的合成插件在 24 次重载、卸载、关闭和 GC 后,Node 24 仍保留全部 25 代模块载荷;激活状态与 Context 已释放。 可卸载的执行所有者,或可回收的可执行产物协议;复用导入 URL 会改变全新模块状态语义。

以下 64 项区分生命周期修复与临时分配优化。数值仅适用于注明的输入及路径,不能相加作为整应用或 RSS 节省量。

优化项 原先行为 优化后行为 效果
01. WorkHub 初始化 启用功能即预加载第二套应用界面。 首次使用才创建,冷启动待渲染器就绪后显示;草稿、附件照常保留。 避免未使用界面的 Renderer/DOM 占用。
02. Bot SDK 加载 Bot 模块提前加载飞书、企业微信和 Slack SDK。 连接启动或凭据检查需要时才加载对应 SDK。 避免未使用集成的 SDK 驻留。
03. 附件预览 卸载或迟到异步结果可能留下预览 URL 与暂存数据。 卸载时回收所持 Blob URL,阻止迟到预览与暂存。 释放失去界面所有者的预览,不丢弃有效草稿。
04. 消息重放记录 历史重放身份记录保留提交、编辑的完整正文。 以正文摘要记录身份,完整结果与重放规则不变。 减少历史正文驻留,不淘汰重放条目。
05. 流式显示回调 rAF 与超时竞争结束后,另一回调仍可能待决。 任一显示触发器执行即取消另一回调。 避免后台绘制暂停时积累过期回调。
06. Agent 图 epoch 历史 epoch 保留完整协调快照。 交接且读者结束后释放快照,仅保留修复所需元数据。 避免旧图快照累积。
07. Transcript 订阅 连接级 Promise 等待保留已释放订阅的快照 Buffer。 使用可移除的关闭监听,订阅结束即解除。 无需关闭连接即可释放快照 Buffer。
08. 浏览器释放防护 Session→epoch 映射长期保留已释放会话键。 仅为待决连接操作保留防护状态。 元数据随待决操作存活,保留删除竞态保护。
09. Root Turn 接纳历史 历史接纳身份保留完整请求正文。 保存摘要与独立身份元数据,活动/恢复结果保持完整。 减少历史正文驻留,重放校验不变。
10. 订阅准备状态 订阅激活后仍持有失败 Promise、resolver 和准备对象图。 激活及追赶队列排空后清除准备状态。 释放旧副本、句柄与快照的引用。
11. Zod 递归解析 Schema 保留旧解析上下文,同步异常可能遗留分配状态。 解除历史上下文引用,异常时清理分配状态。 释放旧解析对象图,保留循环与共享引用语义。
12. 微信 SSE 读取 下一次空闲等待仍持有上一条原始消息、事件与字节块。 再次等待前释放已处理读取值。 去掉末条消息的固定空闲驻留。
13. 原生 WebRTC 生命周期 连接终止后,协商/数据通道任务及 Peer 资源可能继续存活。 由统一连接生命周期管理任务取消与 Peer 关闭。 取消、关闭或 drop 时释放原生连接资源。
14. 本地消息重试 五秒重试闭包持有完整投递作用域与正文。 仅捕获服务和消息键,待投递正文仍由 SQLite 保存。 减少重试等待期间的重复正文驻留。
15. Peer Mesh 定时器 协调提前唤醒后,旧五分钟定时器与监听仍存在。 每轮等待结束即清理自己的定时器和 abort 监听。 避免无效等待积累。
16. 代码语言缓存 不支持的语言标签被缓存为 null,可能连带保留大消息字符串。 只缓存支持的语言定义。 避免任意标签保留旧消息,纯文本回退不变。
17. CLI 事件等待 Run 级 Promise 竞争保留已消费的事件/轮询等待记录。 每次事件读取或图轮询完成即解除失败等待器。 长任务中及时释放已消费记录。
18. CLI 撤销快照 撤销快照反复深拷贝大段粘贴文本。 复制可变容器,共享不可变字符串。 减少撤销占用,不删减历史步骤。
19. 短文本截断 截断前将完整输入展开为码点数组。 只收集预算前缀和一个截断判定字符。 限制临时字符数组,规范化行为不变。
20. 诊断日志截断 短日志输出仍可能分配全文字符数组和 UTF-8 Buffer。 仅收集/编码预算所需文本及标记。 减少临时分配,完整脱敏与字节上限不变。
21. Peer 接收解析器 认证余量、已消费块和空视图仍被引用。 下次等待前释放已消费解析输入。 释放已处理 Buffer,保留半帧与重放数据。
22. Reasoning 正文 从未展开的折叠推理区也挂载完整 Markdown/代码子树。 首次展开才挂载,之后折叠保留节点与状态。 减少初始 DOM/堆占用;完整正文的页内查找/无障碍访问需先展开,保留键盘展开入口。
23. 搜索结果摘要 截断标题/摘要可能保留完整供应商文本的底层字符串。 仅在发生截断时复制保留前缀。 短结果不再连带持有大段原文。
24. 更新验签依赖 启动读取更新通道元数据即加载 Sigstore/TUF。 解析验签包或验证签名时才加载。 仅查询元数据的启动路径不再驻留验签依赖。
25. 自定义模型请求取消 取消链路依赖临时 Request 转发信号的生命周期。 直接传递调用方实际生效的 AbortSignal。 保持取消有效,使废弃请求能够停止。
26. JSON 请求正文覆写 覆写/收尾处理对已缓冲正文再次 clone 并完整读取。 复用首次读取的 ArrayBuffer 解码。 去掉一次完整正文读取与复制。
27. Computer Use 清理 从未使用的会话也创建 generation/观察状态,并保留退役防护。 本地未知会话不通知观察器;generation 防护仅保留到最后活动队列尾结束。 避免历史清理元数据增长。
28. 已删除会话的上传 会话不存在的拒绝响应可能遗留无法使用的暂存 Buffer。 该拒绝分支释放请求连接/Host Epoch 所属暂存数据。 及时回收失效上传,错误与所有权边界不变。
29. 退役 Goal token 计数 归档/删除会话仍保留 token 总量,迟到读取还可能重新写入。 删除退役总量,异步写入前复核控制权。 避免旧计数累积或覆盖新 Goal。
30. MCP 发送背压 受阻发送的观察器持有已结束请求对象图。 隔离取消观察器,请求结算时解除 reject 引用。 释放已结束请求对象图,不丢弃待发字节。
31. 技能清单快照 各缓存回合分别持有内容相同的清单快照。 仅在 revision 与完整内容均一致时共享冻结快照。 减少重复清单存储,保留每回合读取与 100 回合历史。
32. 系统提示词正文 已完成回合分别保留相同基础提示词的独立副本。 共享相等正文,读取与 revision 对象仍独立。 减少重复正文存储,每步动态附加提示词仍保持最新。
33. 有界草稿历史 12 万字符的草稿切片可能保留超大原输入。 发生截断时只复制保留窗口。 释放超大底层字符串,32 条历史上限不变。
34. Responses WebSocket 历史 续接请求重建时深拷贝私有历史输入/输出与新增内容。 新建数组,复用私有历史值。 去掉重复历史深拷贝,对外防御性复制不变。
35. 已完成流式显示 受限的最终显示切片可能保留超大完整输出。 复制最终有界头/尾窗口。 释放完整输出底层字符串,显示与持久化正文不变。
36. 流式重新播种缓冲 有界显示/脱敏恢复切片可能连带保留超大原始增量。 对超大增量复制最终有界字符串。 释放超大底层存储,恢复内容不变。
37. 归档分页 小型返回页可能保留完整归档底层字符串。 在 ArchiveRead 最终返回边界统一克隆一次。 分页与完整归档解除存储关联,分页规则及权限不变。
38. 工具结算批次 工具结算后的私有原始结果仍在后续执行期间存活。 全部结算及有序 Plan/Yield 处理后清空批次。 提前释放已完成结果,持久化事件仍保留完整内容。
39. 已发送提示词历史 规范化后的短发送文本可能保留超大输入底层字符串。 仅在线路规范化缩短文本时复制。 释放被裁剪的底层存储,发送/召回与拒绝草稿不变。
40. 插件隔离标签 强引用驻留表保留已替换/卸载插件版本的标签。 按 Symbol 生命周期弱共享,并防止旧清理回调误删。 GC 后回收无用标签元数据,不改变 ESM 模块驻留。
41. 记忆提取切分候选 尝试首个切分前就构建所有候选的前缀/后缀数组。 按顺序逐个生成,首个可用切分成功即停止。 避免预先进行二次方级引用复制,最坏搜索量不变。
42. 记忆定位预览 生成短预览前将助手全文展开为码点数组。 只收集所需的 2,000 码点前缀。 256 KiB ASCII 回复的数组由 262,144 项降至 2,000 项。
43. 会话目录预览 短目录预览也分配完整消息字符数组。 最多保留 96 个码点,以后一字符判断截断。 预览数组不再随消息全文长度增长。
44. Agent 图交接前缀 受预算限制的交接文本仍展开整段回复字符数组。 仅收集既有字节预算所需前缀。 256 KiB ASCII 回复的数组由 262,144 项降至 16,384 项。
45. 子 Agent 结果包 有界 JSON 输出仍将子 Agent 全文展开为码点。 仅收集输出预算可能容纳的字符。 256 KiB ASCII 回复的数组由 262,144 项降至 32,768 项。
46. 工作区指令 前缀选取与截断判定各展开一次完整指令。 最多收集 6,000 个码点,再检查一个字符。 以单个有界前缀替代两次全文字符数组。
47. PTY 模型输出尾部 尾部裁剪展开完整滚动区,并逐字符构建拼接链。 直接从末尾收集预算内完整码点。 去掉全文滚动区临时数组与拼接链。
48. OAuth 错误正文 读取错误响应克隆体,留下未消费原分支的缓冲。 直接读取一次失败响应正文。 避免刷新等待期间额外持有错误正文 Buffer,成功 SSE 不预读。
49. 稳定文件读取缓冲 小文件也按配置最大长度加哨兵字节分配。 按已准入文件实际大小加一个增长检测字节分配。 881 字节文件由申请 262,145 字节降至 882 字节,身份校验保留。
50. 已消费的帧缓冲 零长度尾视图仍保留最后接收 Buffer 的底层存储。 最后完整帧消费后改用空 Buffer。 释放每条开放连接的末块驻留,半帧尾部仍保留。
51. 完整 IPC 帧复制 解码器将空前缀与已独立持有的输入副本再次拼接。 无待决半帧前缀时直接使用该副本。 完整帧路径减少一次整块复制。
52. Composer 历史初始化 初始化后每次渲染仍读取并解析持久化历史。 每次挂载仅初始化一次历史。 去掉重复存储读取、JSON 解析与被丢弃的历史字符串。
53. Peer 握手/读取生命周期 读取循环等待时仍持有已消费握手余量或末块。 转交一次性余量所有权,等待前释放已消费块。 释放连接级已消费 Buffer,背压与恢复不变。
54. 实时 PTY 分块 每个输入块先生成全文码点数组,再切片分段。 直接遍历 UTF-16 边界,保留每段 4,096 码点规则。 去掉实时输出的全文字符数组及其切片。
55. Base64 字节计数 已验证或本地编码的 Base64 仍仅为计数而解码。 直接计算解码后长度,不创建 Buffer。 去掉纯计数解码;512 KiB 页的解码分配由 1.5 MiB 降至 512 KiB。
56. 研究产物读取窗口 读取字符窗口前将整篇文档展开为数组。 保留全文计数,仅收集请求范围内码点。 去掉全文数组,仅保留默认 3.2 万/最多 6.4 万字符窗口。
57. Edit 行号定位 通过拆分整个前缀与编辑区间计算行位置。 直接统计换行符,不生成行数组。 去掉定位用临时数组,匹配与编辑结果不变。
58. 局部编辑 diff 小型 diff 也将编辑前后完整文件拆成行数组。 仅收集既有 800 行/32 KiB 上限内的局部窗口。 去掉两份全文行数组,保留上下文与 hunk 格式。
59. 部分文件读取 选取少量行也先将完整文件拆成行数组。 Workspace 与文件系统 Worker 共用选定行扫描。 去掉全文行数组,完整文件读取仍保留。
60. 交互回填防护 AppShell 永久保留历史 Session→epoch 键。 改为仅由待决读取持有并负责清理的身份标记。 避免历史防护键增长,保留迟到响应保护。
61. 工具输出行窗口 头尾预览先拆分完整工具输出,再限制行数。 定位边界,仅收集保留行。 去掉全文行数组,行数/字节预算与提示标记不变。
62. 超大单行预览 超大单行先完整 UTF-8 编码,再裁剪。 编码前先选取预算大小的 UTF-16 窗口。 百万字节 ASCII 行仅编码 51,201 字节,边界行为不变。
63. WebFetch 模型前缀 生成受限模型预览前编码整个抓取正文。 只编码含提示标记 50 KiB 预算所需前缀。 4 MiB ASCII 正文仅编码 51,132 字节,完整获取/读取/解析仍保留。
64. Runtime Resource 克隆 先克隆整个 update,再为截断重新克隆其 result。 只克隆一次 update,在其 result 副本上原地截断。 结果深拷贝由两次降至一次,隔离、响应与 revision 不变。

第 22 项测量与取舍

用真实 TurnView 渲染固定合成会话:20 段 reasoning,每段 180 行代码,20 个折叠工具结果及一个最终答复;两版输入 viewmodel JSON 均为 376,472 字节。在独立 Chromium 151 进程中,GC 后 JS heap 为 10,839,440 → 4,895,284 字节(减少约 5.67 MiB),DOM 元素为 5,020 → 679。这是组件驻留测量,不是 OS RSS 或典型用户分布;原文仍保留。旧实现展开前能通过浏览器页内查找命中深层标记,新实现须先展开;展开第一段后恢复 217 个正文后代元素及标记查找。键盘可展开不能抵消这一发现成本。未测试真实屏幕阅读器。维持首次展开挂载,不新增缺乏测量依据的长度阈值。

验证

第二轮跟进 2bb009541:补充请求取消、Goal lease/token 基线、Zod ESM/CJS 循环身份和 WebRTC 拆除行为覆盖。两个受影响 TypeScript 工作区编译通过;18 项定向 JS 测试、Rust 全套 40 项、lint/格式、ASF 文件头及 Rust Clippy 通过。分别移除请求 signal 转发、Goal lease 检查或 Zod handoff 恢复时,反向测试均失败。新增的是契约测试,不是分配量基准;本轮未改生产实现。

Review 后续修复 4bbe8fd3d:五个受影响 TypeScript 项目编译通过;640 项测试通过(Graph/Bot 115、WorkHub 24、Computer Use 57、Peer 14、UI 430)。Graph 与 SDK 回归测试在修复前源码上也确认失败。未新增分配量探针文件。

以下为后续修复之前的原 64 项变更验证:

  • Linux/Node 24:lint、格式、构建、全工作区类型检查、Knip 与仓库规则检查通过;标准工作区测试 9,759 项通过。
  • Runtime Host:默认并发运行有 6 项失败;完整套件在并发 2 下为 1,845 通过、12 跳过、0 失败。默认并发稳定性尚未确认。
  • Desktop E2E:37 项通过;浏览器 smoke:17 项通过;界面对齐:无问题;Storybook:332 个故事/359 次主题渲染通过。
  • Rust 格式、Clippy、许可、声明及测试,发布约束、Linux sandbox、Docker egress proxy/工具链、已发布 CLI 状态目录前向升级验证均通过。
  • npm 11.19 安装、CLI 候选包构建及离线安装后的 CLI smoke 通过。

界面验证命令输出:

37 passed (10.1m)
Storybook render smoke passed (332 stories, 359 theme renders).

未覆盖:Windows/macOS 打包应用全套、真实飞书/企业微信连接及全部平台原生场景。本 PR 不包含分配量专项回归测试。

AI 使用声明

  • 生成式工具没有实质贡献
  • 生成式工具有实质贡献

工具与范围:OpenAI Codex — 实现、验证及 PR 说明。相关提交包含 Generated-by: OpenAI Codex

检查清单

  • 测试覆盖改动,且没有此改动时测试会失败
  • Lint、格式、类型检查及受影响测试套件在本地通过

本 PR 是否改变行为?

  • 是 — 已在上述概要中说明

Create the WorkHub renderer on first use rather than on enable, while retaining drafts after use. Load platform SDKs only when a configured bot starts or Slack credentials are tested.

Generated-by: OpenAI Codex
@github-actions github-actions Bot added the effort/S Under 100 readable lines label Sep 10, 2026
@M4n5ter M4n5ter changed the title perf: incremental memory optimization across Maka / Maka 内存优化 perf: incremental memory optimization across Maka Sep 10, 2026
Track Blob URL ownership before decode and stop asynchronous staging after unmount while preserving hidden drafts and StrictMode replay.

Generated-by: OpenAI Codex
Retain canonical content digests instead of historical submit and queue edit bodies, without evicting epoch-long outcomes or changing pending operations.

Generated-by: OpenAI Codex
Cancel the losing animation frame or timeout after a stream display batch flush, preventing suspended background frames from retaining completed handlers.

Generated-by: OpenAI Codex
Fence old supervisor callbacks and preserve explicit reconcile readers across epoch handover. Retain lightweight driver metadata for historical projection repair and close diagnostics.

Generated-by: OpenAI Codex
Replace connection-lifetime Promise races with removable abort listeners so released transcript overlay buffers can be collected while the connection remains open.

Generated-by: OpenAI Codex
Drop per-session release epochs when connection attempts settle and avoid retaining tombstones for sessions that never acquired a browser, preserving reentrant teardown and single-flight fences.

Generated-by: OpenAI Codex
Retain canonical content digests and owned identity metadata for historical admissions while keeping full current tips and validated recovery or conflict results.

Generated-by: OpenAI Codex
Release the SQLite lease before removing temporary files so root admission tests clean up on Windows and do not retain fixture database owners.

Generated-by: OpenAI Codex
Keep frame cleanup within the renderer architecture boundary without widening legacy budgets.

Generated-by: OpenAI Codex
Keep cycle memoization scoped to each parse and restore allocation state when parsing throws.

Generated-by: OpenAI Codex
Generated-by: OpenAI Codex
Keep iterator result and decoded message lifetimes within each completed read. Preserve reconnect cursors, parser boundaries and iterator cleanup semantics.

Generated-by: OpenAI Codex
Cancel connection-owned channel workers and close the native peer on explicit close, unpolled muxer drop and failed or aborted upgrades. Keep successful connection lifetime separate from signaling-attempt cancellation.

Generated-by: OpenAI Codex
Keep retry callbacks outside the delivery lexical context so pending or completed attempts do not retain parsed message bodies for the five-second retry interval. Preserve durable intents, timer behavior, and replay identity.

Generated-by: OpenAI Codex
Own the five-minute timer and abort listener in each reconciliation wait. Early public triggers release both immediately while normal expiry and endpoint shutdown still wake the loop.

Generated-by: OpenAI Codex
Keep local restart and retry assertions running on Windows without equating stat mode bits with ACL permissions. Production permission handling is unchanged.

Generated-by: OpenAI Codex
Generated-by: OpenAI Codex
Copy mutable editor state and paste maps without deep-cloning each stored paste for every typed word. Preserve all undo steps and cover public editing, paste renumbering, and retained heap growth.

Generated-by: OpenAI Codex
Collect only the code-point prefix needed for nonnegative budgets while preserving cleaning and numeric slice semantics. Cover transient allocations and Unicode behavior with regression tests.

Generated-by: OpenAI Codex
Preserve complete redaction and byte-budget semantics while limiting temporary code-point arrays and UTF-8 encoding to the useful prefix.

Generated-by: OpenAI Codex
Keep startup channel metadata free of Sigstore and TUF module graphs. Load the parser and verifier only when an update is actually verified, preserving fail-closed trust checks.

Generated-by: OpenAI Codex
Forward the caller's effective AbortSignal instead of a temporary Request's GC-sensitive derived signal. Preserve explicit null, inherited and overriding signals on every body path, and cover prompt native HTTP cancellation after GC.

Generated-by: OpenAI Codex
Encode only the model-visible prefix plus a surrogate guard unit, preserving the byte cap and truncation marker. Cover real Host HTTP fetches, privacy refusal, Unicode boundaries and old-source allocation regression.

Generated-by: OpenAI Codex
Clone each runtime resource update once before bounding its state. Preserve source isolation and query revisions, and cover get/list allocation with old-source negative controls.

Generated-by: OpenAI Codex
Remove 65 new test and fixture files plus added allocation probes from existing suites. Keep only minimal existing-test behavior and cleanup adaptations. Preserve production memory fixes and remove stale test references.

Generated-by: OpenAI Codex
Preserve dynamic plugin prompt composition and share only completed equal base prompts and combined skill inventories.

Generated-by: OpenAI Codex
Re-pin the compatible byte-count optimization to main's protocol epoch, record the MCP dependency patch license, and correct the renderer token ledger to the checked source.

Generated-by: OpenAI Codex
@M4n5ter
M4n5ter marked this pull request as ready for review September 11, 2026 02:10
@github-actions github-actions Bot added effort/XL Under 2500 readable lines and removed effort/S Under 100 readable lines labels Sep 11, 2026

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed across six dimensions: dependency patches, lifetime/ownership, Rust WebRTC, text truncation, transport framing, Desktop/UI. Differential fuzzing confirmed the 13 rewritten truncation functions and the four framing changes are behaviour-preserving; cargo test webrtc_direct passes. Most of the 64 items are correct equivalent rewrites, and several fix real retention (Path.remainder, the AppShell session→epoch map, the Promise.race reaction in session continuity, zod's lastCtx).

The findings below cannot be fixed at a single line. Line-level ones are filed as inline comments.

P1 — Collapsed reasoning bodies leave the DOM without a decision

packages/ui/src/astryx-chat-reasoning.tsx

Collapse was grid-template-rows: 0fr + overflow: hidden, not display: none. Content stayed in the accessibility tree and stayed findable with Cmd+F. Item 22 removes it until first expansion, so never-opened thinking text is unreachable for screen readers and find-in-page. Streaming collapsed blocks are worse: previewText is gated on !isStreaming, so they expose only "Thinking".

The PR resolved this by editing an existing test to click the header before reading .maka-chat-reasoning-content. That test previously read the text without clicking — the DOM contract changed and the change was absorbed rather than raised.

Per CONTRIBUTING, removing a capability or changing an interaction needs a decision in an issue or Discussion first. Either open one stating the a11y/find-in-page cost this buys, or keep the subtree mounted with hidden="until-found" — which preserves find-in-page but not the memory this item is after.

P1 — Identity comparison for idempotent replay now has two authorities

packages/runtime-host/src/server/message-coordinator.ts, packages/runtime-host/src/server/root-admission-owner.ts

Items 04 and 09 are described as storing less. They also replace the conflict predicate: isDeepStrictEqual(payload) / messageContentsEqual(content) become digest comparison. Getting this wrong either duplicates a delivery or rejects a legitimate resend as operation_conflict — a reconnect-and-resend path.

The two rule sets do line up field for field today. But messageContentsEqual and normalizeMessageContent are written separately in events.ts, and nothing binds them. Editing either silently breaks idempotent replay with no failing test.

Make messageContentsEqual call messageContentDigest on both sides so there is one rule. That also removes the superseded predicate rather than leaving both.

P2 — Releasing a reference and closing in order are treated as one primitive

Two items make the same mistake in opposite directions.

Item 06, packages/runtime/src/stream-graph-coordinator.ts:1453#retireDriver awaits driver.stopTask, the operator teardown I/O, with no timeout and no abort (beginDrain's abort only reaches driver.abortController). Its only production caller is prepareFreshAgentGraphEpoch, so every new Turn in graph/swarm mode now starts behind the previous epoch's shutdown. Hang one operator in teardown and the next Turn cannot start; on main the epoch advances regardless.

Item 13, native/runtime-host-peer/src/webrtc_direct/lifetime.rs:48close() cancels before peer_connection.close(), cutting off workers still inside drive_data_channel and discarding both the per-substream outgoing queue and the up-to-5s drain_data_channel flush. On main, close and the workers ran concurrently, so drain could complete.

Dropping a reference is synchronous, has no side effects, and can always happen immediately. Closing in order is asynchronous, does I/O, and can fail or hang. Item 06 waited for a close under the name of releasing; item 13 cancelled under the name of closing. Both are one line, but fixing them separately leaves the distinction unstated.

void this.#retireDriver(driver) works for 06 — Promise.allSettled already absorbs the errors — or bound the stopTask wait. For 13, move the cancel after peer_connection.close().await; Drop should keep cancelling first, since that path has no graceful-close semantics.

P2 — A shared invariant is rederived at 13 call sites

Items 42–63 all rest on the same fact: a code point encodes to at least one UTF-8 byte, so collecting budget + 1 UTF-16 units before slicing by bytes is safe. Every derivation is correct — but each is local.

The cost is already visible: tool-output.ts guards degenerate budgets to preserve Buffer.subarray's negative-end semantics, web-fetch-tool.ts uses the same technique without the guard. Unreachable today because the budget is a constant; it is what rederiving the same reasoning 13 times produces.

text-line-window.ts is the counter-example — it merges two duplicated line-window implementations into one authority. Same treatment for a boundedPrefixByBytes(text, maxBytes) would let most of these call sites shrink to one call. A smaller instance: use-composer-history.ts fixes a non-lazy useRef, use-composer-attachments.ts keeps one; a shared useLazyRef covers both.

P2 — Canonical-base64 validation and length measurement are separable

Buffer.byteLength(s, 'base64') equals the decoded length only for canonical input — Node computes (len - padding) * 3 >>> 2 without checking the alphabet, whitespace, or a multiple-of-four length. All four call sites validate first, so item 55 is correct as written.

Nothing in the signature carries that precondition, and the input is peer-controlled. A future call site that skips validation lets a peer pass SESSION_TRANSCRIPT_PAGE_MAX_BYTES while decoding to a different size. A canonicalBase64ByteLength(value) that validates and measures together removes the gap and drops one duplicated regex.

P2 — The PR's central premise is an unspecified engine detail, stated five times

Five sites rely on structuredClone(<string>) to detach a slice from an oversized backing string. That is a V8 value-serializer behaviour, not a language guarantee, and items 23/33/35/36/37/39 all depend on it.

Right now the same justification is repeated in five comments with no measurement. One detachString(s) helper, explained once, plus a single retained-size heap snapshot in the PR body would make the premise checkable.

P2 — The tests for these changes exist in this branch's history

44bc559f6 removes 65 test files and 11,509 lines. That includes the behaviour-asserting ones this review would otherwise ask for: zod-memoizer-lifetime.test.ts, webrtc_direct/lifecycle_tests.rs, request-customization-abort.test.ts, goal-token-lifetime.test.ts, stream-graph-coordinator-retirement.test.ts, computer-use-session-lifetime.test.ts, file-read-line-window-allocation.test.ts, edit-diff-window-allocation.test.ts.

Dropping the allocation probes is reasonable — they are unstable across Node versions and Electron, which the earlier make prompt heap checks portable to Electron commit already shows. The eight above assert behaviour, not allocation counts, and fail on the old code. Restoring them from 44bc559f6^ costs far less than rewriting them.

P3 — Three body entries do not match the diff

  • 07 describes connection-lifetime promise waits becoming removable close listeners. The diff for session-transcript.ts and session-transcript-pager.ts contains only the Buffer.byteLength change.
  • 31/32 describe sharing frozen snapshots and completed prompt strings. The actual change bounds code-point array allocation.
  • 27 omits that applyServiceRelease no longer notifies onSessionInvalidated for locally unknown sessions.

In a body that enumerates 64 items, mismatches cost the reader confidence in the other 61.

P3 — Three behaviour fixes are filed as allocation work

Item 25 restores cancellation through request customization, item 27 adds the known gate, and the two identity changes above alter a predicate. Each is a fix worth having and worth its own commit and body entry; under "reduce redundant allocations" they get skimmed.

P3 — The three new patches have no upstream exit

patches/README.md entries reference upstream issues elsewhere (See #2978, See #1967 / #1976, See #3446). The zod, MCP and pi-tui entries have none, and their stated exit conditions describe upstream behaviour changes that nobody upstream has been asked for. The zod parse-state leak and the MCP settled-observer retention are general bugs, not Maka-specific needs.


中文

从六个维度评审:依赖补丁、生命周期与所有权、Rust WebRTC、文本截断、传输分帧、Desktop/UI。差分 fuzz 确认 13 个被重写的截断函数和四处分帧改动行为不变,cargo test webrtc_direct 通过。64 项里大部分是正确的等价改写,其中几项修的是真实驻留(Path.remainder、AppShell 的 session→epoch map、session continuity 的 Promise.race reaction、zod 的 lastCtx)。

以下问题无法在单行修复,能局部修的已作为行内评论提出。

P1 — 折叠态 reasoning 正文离开 DOM,且未经定案

packages/ui/src/astryx-chat-reasoning.tsx

折叠原本是 grid-template-rows: 0fr + overflow: hidden,不是 display: none,内容留在可访问性树里、能被 Cmd+F 命中。第 22 项改成首次展开才挂载,未展开的思考文本对屏幕阅读器和页内查找不可达。流式折叠块更差:previewText!isStreaming 条件,只剩一个 "Thinking"。

PR 的处理方式是改一个既有测试,让它先点 header 再读 .maka-chat-reasoning-content。那个测试原来不点击就能读到文本——DOM 契约变了,改动被吸收而不是被提出。

按 CONTRIBUTING,删功能或改交互须先在 issue/Discussion 定案。要么开一个说明这是拿 a11y 和查找可达性换内存,要么用 hidden="until-found" 保留挂载——但那样保不住这一项想要的内存。

P1 — 幂等重放的身份比较现在有两个权威

packages/runtime-host/src/server/message-coordinator.tspackages/runtime-host/src/server/root-admission-owner.ts

04 和 09 的描述是少存正文,实际还换了冲突判据:isDeepStrictEqual(payload) / messageContentsEqual(content) 变成摘要比较。判错一边是重复投递,另一边是把合法重发误判为 operation_conflict——断线重连重发是正常路径。

两套规则今天逐字段对齐。但 messageContentsEqualnormalizeMessageContentevents.ts 里各写各的,没有任何东西绑定它们,改动任一边都会静默打破幂等重放且没有测试会失败。

messageContentsEqual 两边都调 messageContentDigest,规则只剩一份,也顺带删掉被替代的判据。

P2 — 「放弃引用」和「有序关闭」被当成同一个原语

两项在相反方向上犯了同一个错误。

第 06 项,packages/runtime/src/stream-graph-coordinator.ts:1453——#retireDriverdriver.stopTask,那是 operator 的 teardown I/O,没有超时也没有 abort(beginDrain 的 abort 只作用于 driver.abortController)。它唯一的生产调用方是 prepareFreshAgentGraphEpoch,于是 graph/swarm 模式下每个新 Turn 都排在上一个 epoch 的关闭之后。某个 operator 卡在 teardown,下一个 Turn 就起不来;main 上 epoch 会直接推进。

第 13 项,native/runtime-host-peer/src/webrtc_direct/lifetime.rs:48——close()peer_connection.close() 之前 cancel,掐断仍在 drive_data_channel 里的 worker,丢掉 per-substream 的 outgoing 队列和最长 5s 的 drain_data_channel flush。main 上 close 和 worker 是并发的,drain 有机会跑完。

放弃引用是同步的、无副作用的、永远可以立即做;有序关闭是异步的、有 I/O、可能失败也可能卡住。第 06 项以释放的名义等了关闭,第 13 项以关闭的名义做了取消。两处都只是一行,但分开修就说不出这个区别。

06 用 void this.#retireDriver(driver) 即可(Promise.allSettled 已经吞掉异常),或给 stopTask 的等待加上界。13 把 cancel 移到 peer_connection.close().await 之后;Drop 里保持先 cancel,那条路径本来就没有优雅关闭语义。

P2 — 同一条不变量在 13 个调用点各推导一次

42–63 全部建立在同一个事实上:一个码点至少编码成一个 UTF-8 字节,所以先收 budget + 1 个 UTF-16 单元再按字节切是安全的。每处推导都对,但都是局部的。

代价已经出现:tool-output.ts 为退化预算加了守卫以保留 Buffer.subarray 负数 end 的语义,web-fetch-tool.ts 同样的技巧没加。今天不可达(预算是常量),但这就是同一推理写 13 遍的结果。

text-line-window.ts 是反例——它把两处重复的行窗口实现合并成一个权威。给 boundedPrefixByBytes(text, maxBytes) 同样处理,多数调用点能缩成一次调用。小号版本:use-composer-history.ts 修了非惰性 useRefuse-composer-attachments.ts 留着;一个共享的 useLazyRef 覆盖两处。

P2 — 规范 base64 的校验与测长可以被分开

Buffer.byteLength(s, 'base64') 只在输入规范时等于解码长度——Node 算的是 (len - padding) * 3 >>> 2,不检查字母表、空白和长度是否 4 的倍数。四个调用点都先校验,所以第 55 项本身是对的。

但签名里没有携带这个前置条件,而输入是 peer 可控的。将来某个跳过校验的调用点会让 peer 通过 SESSION_TRANSCRIPT_PAGE_MAX_BYTES 而实际解码出别的长度。一个 canonicalBase64ByteLength(value) 把校验和测长写在一起即可消除这个缺口,顺带删掉一处重复正则。

P2 — PR 的核心前提是一个未规范化的引擎细节,且被陈述了五遍

五处依赖 structuredClone(<string>) 把切片从超大 backing string 上摘下来。这是 V8 value-serializer 的行为,不是语言保证,而 23/33/35/36/37/39 全部依赖它。

现在同一个理由在五条注释里各写一遍,没有测量。一个 detachString(s)、解释一次,加上 PR 正文里一次 retained size 的堆快照,这个前提就可核对了。

P2 — 这些改动的测试就在本分支的历史里

44bc559f6 删掉 65 个测试文件、11509 行,其中包括本次评审本来要求补的那些行为断言型测试:zod-memoizer-lifetime.test.tswebrtc_direct/lifecycle_tests.rsrequest-customization-abort.test.tsgoal-token-lifetime.test.tsstream-graph-coordinator-retirement.test.tscomputer-use-session-lifetime.test.tsfile-read-line-window-allocation.test.tsedit-diff-window-allocation.test.ts

删掉 allocation probe 是合理的——它们跨 Node 版本和 Electron 不稳定,之前那个 make prompt heap checks portable to Electron commit 已经说明了。但上面八个断言的是行为不是分配量,在旧代码上会失败。从 44bc559f6^ 挑回来,成本远低于重写。

P3 — 正文三处与 diff 不符

  • 07 描述连接级 promise 等待改成可移除的 close 监听。session-transcript.tssession-transcript-pager.ts 的 diff 里只有 Buffer.byteLength 替换。
  • 31/32 描述共享冻结快照和已完成 prompt 字符串,实际改动是码点数组的分配上限。
  • 27 漏掉了 applyServiceRelease 不再为本地未知会话通知 onSessionInvalidated

在一份逐条列举 64 项的正文里,对不上的条目会让读者对其余 61 条失去信心。

P3 — 三处行为修复被归在分配优化下

第 25 项恢复了请求定制路径的取消,第 27 项加了 known 门,上面两处身份比较换了判据。每一项都值得做,也都值得有自己的 commit 和正文条目;放在「减少重复分配」下会被略过。

P3 — 三个新补丁没有上游出口

patches/README.md 里既有条目都引用了上游(See #2978See #1967 / #1976See #3446)。zod、MCP 和 pi-tui 三条都没有,而它们写的退出条件描述的是上游的行为变化,但没人向上游提过。zod 的解析状态泄漏和 MCP 的已结算观察者驻留都是通用 bug,不是 Maka 特有需求。

return;
}

const require = createRequire(import.meta.url);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 — packages/runtime/src/bots/slack-bridge.ts:90
(same at feishu-bridge.ts:99, wecom-bridge.ts:94)

The require sits outside the try that follows. A resolution failure — trimmed install, missing nested @slack/socket-mode — now throws out of start(), skipping recordFailure, readiness = 'degraded' and emitStatusChange(). Before item 02 this failed at module-graph load: once, at startup, loudly. Now it fails when the user clicks connect and the bot status never updates.

bot-test.ts:97 puts its require inside the try. Move these three in as well.

中文

require 在随后的 try 之外。模块解析失败(安装被裁、嵌套的 @slack/socket-mode 缺失)会裸抛出 start(),跳过 recordFailurereadiness = 'degraded'emitStatusChange()。第 02 项之前这是模块图加载期的失败:启动时一次性、明显。现在变成用户点连接才失败,而 bot 状态不更新。

bot-test.ts:97require 就在 try 里,这三处照做。

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

English

Accepted and fixed in 4aa979987. SDK resolution and client construction now run inside each bridge's failure boundary, including the case where no client was constructed. Loading remains synchronous, so this does not add a stop/loading await race.

The registry regression forces the three SDK resolutions to fail and checks the emitted failure statuses. It fails against the pre-follow-up source and passes now. Slack retains its existing degraded/rethrow behavior; Feishu and WeCom retain their configured failure state. This is a connection-failure handling defect, not a failure of every normally installed connection.

Automated follow-up by OpenAI Codex.

中文

接受,已在 4aa979987 修复。SDK 解析与客户端构造现已纳入各 bridge 的失败处理边界,也覆盖尚未构造客户端就失败的情况。加载仍同步执行,没有新增 stop 与模块加载 await 的竞态。

回归测试通过 registry 强制三种 SDK 解析失败,并检查实际发出的失败状态;在修复前源码上失败,当前通过。Slack 保留原有 degraded/重新抛出行为,飞书和企业微信保留 configured 失败状态。这是连接失败处理缺陷,不代表正常安装下的所有连接都会失败。

OpenAI Codex 自动跟进回复。

@@ -608,13 +608,9 @@ export function createWorkHubPresentation(deps: WorkHubPresentationDeps) {
const enabled = deps.isEnabled();
if (disposed) return;
if (enabled) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 — apps/desktop/src/main/workhub-presentation.ts:610

detach() now runs ensureView()show()focusComposer(), and focusComposer returns early on !rendererReady, leaving only focusPending. The floating window is transparent: true, backgroundColor: '#00000000', so until the renderer loads the user sees an empty rounded frame with no loading state, and keystrokes in that window are dropped because webContents has no focus.

The main-window path calls ensureView() when the dock is visible, but the shortcut path deliberately bypasses the main window — the test at :571 asserts mainRequests === 0. So "Desktop closed, press the shortcut" has no warm-up at all.

requestProgress() / progress-ready already implements "renderer confirms it painted, then showInactive". Reuse it, or keep a prewarm triggered on first shortcut registration rather than removing it.

中文

detach() 现在是 ensureView()show()focusComposer(),而 focusComposer!rendererReady 时直接 return,只置 focusPending。floating 窗口是 transparent: true, backgroundColor: '#00000000',渲染器加载完之前用户看到一个空的圆角框,没有 loading 态,期间的键入因 webContents 未 focus 而丢失。

主窗口路径在 dock 可见时会调 ensureView(),但快捷键路径刻意绕开主窗口——:571 的测试断言 mainRequests === 0。所以「Desktop 没开时按快捷键」完全没有预热。

requestProgress() / progress-ready 已经实现了「渲染器确认画完再 showInactive」。复用它,或者把 prewarm 改成首次注册快捷键后触发而不是删掉。

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

English

Accepted the cold-start presentation gap; fixed in e2721989f without restoring background prewarming.

A cold floating summon stays hidden until the existing renderer-ready handshake, then shows and focuses the mounted composer. The existing focusPending state owns that pending summon: a second shortcut or disabling cancels it, and passive progress cannot replace it. Once ready, warm shortcut show/hide remains synchronous.

All 24 presentation tests pass, including no renderer from enabling alone, no window focus during cold loading, ready-triggered presentation, cancellation before ready, and the existing docking/progress/animation behavior. This validates the controller behavior, not an all-platform packaged-app cold-start latency measurement.

Automated follow-up by OpenAI Codex.

中文

接受冷启动展示空档的问题,已在 e2721989f 修复,不恢复后台预热。

冷启动浮窗会保持隐藏,直到现有 renderer-ready 握手完成,再显示并聚焦已挂载的输入框。复用 focusPending 管理待显示请求:第二次快捷键或禁用可以取消它,被动进度展示也不能替换它。就绪后的快捷键显示/隐藏仍同步完成。

24 项展示控制测试全部通过,包括仅启用不创建 renderer、冷加载不抢焦点、ready 后展示、ready 前取消,以及原有停靠/进度/动画行为。这是控制器行为验证,不冒称测量了所有平台打包应用的冷启动耗时。

OpenAI Codex 自动跟进回复。

];
for (const sessionId of sessions) {
// Unknown cleanup must not create tool-layer state through the observer.
const known =

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 — packages/computer-use/src/maka-cu-backend.ts:835

Scoping sessionGenerations to live operations is equivalent — I traced the fence both ways. But the known gate in the same hunk is a separate behaviour change: applyServiceRelease used to call opts.onSessionInvalidated unconditionally and now skips locally unknown sessions. Reachable after clearSession(S) has removed S from begunSessions, when a maka-cu crash fires generationReleased with S still in sessionIds.

The comment states this as a bug being fixed, and I think the new behaviour is right. It needs a regression test: begin a session, run an action to completion so sessionGenerations empties, then deliver a generationReleased release naming it, and assert the onSessionInvalidated call count against the new contract.

中文

sessionGenerations 收敛到在飞操作是等价的,fence 的两个方向我都推过。但同一 hunk 里的 known 门是独立的行为改动:applyServiceRelease 原来无条件调 opts.onSessionInvalidated,现在对本地未知的会话跳过。可达路径:clearSession(S) 已把 S 从 begunSessions 移除后,maka-cu 崩溃触发 generationReleasedsessionIds 里仍带 S。

注释把它写成在修一个 bug,我认为新行为是对的。它需要一个回归测试:begin 一个会话,跑一个 action 到结束使 sessionGenerations 清空,然后投递一个指名该会话的 generationReleased release,按新契约断言 onSessionInvalidated 的调用次数。

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

English

The notification boundary is intentional, and it is now explicit in item 27. 4bbe8fd3d extends the existing backend lifecycle test rather than restoring a separate allocation-probe file.

It checks that clearing a known session notifies once, clearing it again or clearing a never-begun session does not notify, and a still-known session whose observation has completed is invalidated when the owned mock executor is killed. The 57 backend tests pass.

One correction to the proposed scenario: completion empties the operation fence but does not remove begunSessions. The locally unknown case needs a clear/retirement step; begin → completed action → generationReleased should still notify.

Automated follow-up by OpenAI Codex.

中文

通知边界是有意修改,现已在第 27 项明确写出。4bbe8fd3d 扩充现有 backend 生命周期测试,没有恢复独立的分配量探针文件。

测试确认:清理已知会话通知一次;重复清理或清理从未 begin 的会话不再通知;已完成 observation、但仍已知的会话,在测试自有 mock executor 被终止后仍会失效通知。57 项 backend 测试通过。

建议场景有一点需要修正:操作结束会释放 operation fence,但不会删除 begunSessions。本地未知场景需要 clear/retirement;begin → 操作完成 → generationReleased 仍然应当通知。

OpenAI Codex 自动跟进回复。

}
}

private async processNextLiveMessages(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 — packages/runtime/src/bots/wechat-bridge.ts:214

This file is +87/−27, far beyond the other three bridges, and the SSE rewrite has nothing to do with lazy SDK loading. It is ~70 lines reimplementing for await's completion protocol by hand.

The protocol is correct — closeIterator is false when next() rejects, true when the yield throws, cleared after the catch closes, and readSseTextChunk closes itself on a decode throw. No double close, no leaked iterator. But the claimed gain is a comment about V8 frame liveness with no heap snapshot behind it. Removing processNextLiveMessages / readSseTextChunk / closeIterator and restoring the two for await loops loses nothing measurable against this PR's stated goal.

Either attach a before/after snapshot for a bot process holding large SSE chunks, or revert this file's streaming rewrite. Either way it does not belong inside item 02.

中文

这个文件 +87/−27,远超其他三个 bridge,而 SSE 重写和 SDK 惰性加载毫无关系。它是约 70 行手工重实现 for await 的完成协议。

协议本身是对的——next() reject 时 closeIterator 为 false,yield 抛出时为 true,catch 关闭后置回 false,readSseTextChunk 在 decode 抛出时自行关闭。没有双重关闭,没有泄漏的 iterator。但声称的收益只是一条关于 V8 栈帧活跃度的注释,背后没有堆快照。删掉 processNextLiveMessages / readSseTextChunk / closeIterator 并恢复两处 for await,按本 PR 的既定目标衡量没有任何可度量的损失。

要么附上 bot 进程持有大 SSE chunk 时的前后快照,要么回退本文件的流式重写。无论哪种,它都不属于第 02 项。

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

English

Keeping the streaming rewrite. It is item 12, introduced separately by d02caf9, not part of SDK-loading item 02.

A fresh Node 24.18.1 A/B run of the actual readSseJsonObjects source disproves “nothing measurable”: after consuming an 8 MiB SSE byte chunk and waiting for the next chunk, arrayBuffers is 8,535,450 bytes before vs 146,823 bytes after (both start at 146,823). Ending the old stream returns it to baseline. This is last-chunk idle retention, not a growing history or an RSS claim.

The comparison uses a controlled AsyncIterable and forced GC, not a live WeChat deployment. It isolates the source before d02caf9 against the current implementation. Since the iterator completion behavior is also correct, reverting would reintroduce the measured retention.

Automated follow-up by OpenAI Codex.

中文

保留流式读取重写。它属于第 12 项,由 d02caf9 独立引入,不属于 SDK 加载的第 02 项。

重新在 Node 24.18.1 上对实际 readSseJsonObjects 源码做 A/B:消费一个 8 MiB SSE 字节块、等待下一块时,arrayBuffers 为修改前 8,535,450 字节、修改后 146,823 字节,两者起点均为 146,823。旧流结束后回到起点。因此“没有可度量损失”不成立;这是末块空闲驻留,不是历史持续增长,也不是 RSS 节省承诺。

该对照使用受控 AsyncIterable 和强制 GC,不是真实微信部署;比较 d02caf9 之前的源码与当前实现。迭代器完成协议也正确,回退会重新引入已测出的驻留。

OpenAI Codex 自动跟进回复。

@@ -208,6 +212,20 @@ export function useComposerAttachments(options: {
// Live mirror of every staged item's key, for async preview arrivals to
// check before writing: state snapshots inside a .then are stale by design.
const lifecycleRef = useRef(new ComposerAttachmentLifecycle());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 — packages/ui/src/use-composer-attachments.ts:214

useRef's argument is not lazy: every render constructs a ComposerAttachmentLifecycle — a Set, a Map, a private Set — and discards it. This PR adds readonly previewUrls = new Map() to that class, so each keystroke in the composer now produces more garbage than before.

use-composer-history.ts fixes exactly this pattern in the same PR. Use useRef<ComposerAttachmentLifecycle | null>(null) with lazy assignment, or a shared useLazyRef for both sites.

中文

useRef 的参数不是惰性的:每次渲染都构造一个 ComposerAttachmentLifecycle(一个 Set、一个 Map、一个私有 Set)然后丢弃。本 PR 给这个类新增了 readonly previewUrls = new Map(),于是 composer 里每次按键产生的垃圾比改前更多。

同一个 PR 的 use-composer-history.ts 修的正是这个模式。用 useRef<ComposerAttachmentLifecycle | null>(null) 加惰性赋值,或者给两处共用一个 useLazyRef

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

English

Accepted and fixed in 4bbe8fd3d: the lifecycle now uses a mount-scoped lazy initializer:

const [lifecycle] = useState(() => new ComposerAttachmentLifecycle());

Its mutable containers and existing cleanup behavior are unchanged; unrelated renders no longer construct a discarded lifecycle. No shared useLazyRef abstraction was added. The complete UI suite passes: 430 tests.

Automated follow-up by OpenAI Codex.

中文

接受,已在 4bbe8fd3d 修复,改用挂载期惰性初始化:

const [lifecycle] = useState(() => new ComposerAttachmentLifecycle());

可变容器及既有清理行为保持不变,普通重新渲染不再构造并丢弃 lifecycle。没有新增通用 useLazyRef 抽象。完整 UI 套件 430 项通过。

OpenAI Codex 自动跟进回复。

for (const listener of this.#endListeners) listener();
return;
}
this.#emitData(chunk);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 — packages/runtime-host/src/transport/peer-native.ts:501
(same at resumable-peer-stream.ts:277, :295)

chunk is declared inside the while body, so each iteration has its own binding and it is unreachable once the iteration ends; the next await happens under a fresh binding. The only closure in the peer-native loop captures a native promise resolve, which does not pin the block context. These three assignments are no-ops.

The comment backs them up — "drop consumed chunks" describes the no-op. The other half, the Buffer.alloc(0) tail replacement, is real. So is initialData = Buffer.alloc(0) at :487, where initialData is a parameter alive across the loop's awaits; that one is load-bearing and worth its comment.

Drop the three assignments (let back to const) and narrow the comment to the tail replacement.

中文

chunk 声明在 while 体内,每轮迭代是独立绑定,迭代结束即不可达;下一次 await 发生在新绑定下。peer-native 这个循环里唯一的闭包捕获的是原生 promise 的 resolve,不会钉住块上下文。这三个赋值是 no-op。

注释为它们做了背书——"drop consumed chunks" 说的就是这个 no-op。另一半,Buffer.alloc(0) 的尾部替换,是真的。:487 的 initialData = Buffer.alloc(0) 也是真的,那里 initialData 是形参,跨循环的多次 await 存活;那行是 load-bearing 的,值得有注释。

删掉三个赋值(let 改回 const),把注释收窄到尾部替换。


P3 — packages/runtime-host/src/transport/peer-native.ts:413

remainder: buffered.subarray(newline + 1) is the zero-length-view pattern item 50 fixes. Most handshakes have no trailing bytes, so this pins the handshake backing store (up to AUTHENTICATION_MAX_BYTES).

Bounded and short-lived — #pump replaces it with Buffer.alloc(0) immediately, and #pumpRead drops it on the first Buffer.concat — so not a leak. But the same length === n ? Buffer.alloc(0) : subarray(n) form is applied at two sibling sites; applying it here keeps them consistent.

中文

remainder: buffered.subarray(newline + 1) 正是第 50 项修的那个零长视图模式。多数握手没有尾随字节,所以这里钉住整个握手 backing store(上限 AUTHENTICATION_MAX_BYTES)。

有界且短命——#pump 立刻用 Buffer.alloc(0) 替换,#pumpRead 在第一次 Buffer.concat 时丢掉——所以不是泄漏。但同样的 length === n ? Buffer.alloc(0) : subarray(n) 已经用在两个兄弟位置,这里也照做才一致。

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

English

Keeping all three clear assignments: lexical block scope does not guarantee that V8 clears a suspended async frame's backing references.

On Node 24.18.1, actual-source A/B runs remove the assignments and change let back to const, leaving the remainder fixes intact:

Consumed/idle state Current Requested removal
Resumable DATA, consumed and ACKed native 65,536-byte block collected retained
Partial frame completed and consumed prior 64,000-byte block collected retained
RuntimeHostPeerByteStream, onData completed 65,536-byte block collected retained

These use controlled native-stream reads, WeakRefs and forced GC; legitimate unread/replay buffers remain owned in both variants. The assignments therefore are not no-ops on the target engine.

Accepted the separate empty authentication-tail improvement in 4bbe8fd3d: a fully consumed line now returns Buffer.alloc(0), while nonempty remainder bytes are preserved. The 14 peer transport tests pass.

Automated follow-up by OpenAI Codex.

中文

保留三处清空赋值:词法块作用域不能保证 V8 已清除挂起 async 栈帧中的底层引用。

在 Node 24.18.1 上使用实际源码做 A/B,删除赋值并将 let 改回 const,其他 remainder 修复保持不变:

已消费/空闲状态 当前实现 按建议删除后
Resumable DATA 已消费并 ACK 原生 65,536 字节块已回收 仍保留
部分帧完成且已消费 先前 64,000 字节块已回收 仍保留
RuntimeHostPeerByteStream 的 onData 已结束 65,536 字节块已回收 仍保留

实验使用受控原生流读取、WeakRef 与强制 GC;合法的未读/重放缓冲在两版中都继续保留。因此这些赋值在目标引擎上并非 no-op。

另一条认证空尾建议已接受,在 4bbe8fd3d 中,整行消费完改为返回 Buffer.alloc(0),非空 remainder 字节保持不变。14 项 Peer 传输测试通过。

OpenAI Codex 自动跟进回复。


// Match split('\n').slice(offset, end).join('\n'), without creating entries
// for lines outside the requested window. Empty and trailing lines count too.
export function readTextLineWindow(content: string, offset?: number, limit?: number): string {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 — packages/runtime/src/text-line-window.ts:22

The comment says it matches split('\n').slice(offset, end).join('\n') but not where end comes from. const end = limit ? start + limit : lineCount treats limit === 0 as unbounded and returns everything after offset. Both current call sites already had that falsy semantic, so this is not a regression — but this is now a shared authority, and someone reading the comment will write readTextLineWindow(content, 0, 0) expecting an empty string.

Document the limit === 0 case, or switch to limit === undefined ? lineCount : start + limit after checking whether either call site's schema permits 0.

中文

注释说它等价于 split('\n').slice(offset, end).join('\n'),但没说 end 从哪来。const end = limit ? start + limit : lineCountlimit === 0 当作不限,返回 offset 之后的全部内容。两个现有调用点本来就是这个 falsy 语义,所以不是回归——但它现在是共享权威,照注释理解的人会写 readTextLineWindow(content, 0, 0) 并期望空串。

limit === 0 的语义写进注释,或者改成 limit === undefined ? lineCount : start + limit,改之前先确认两个调用点的 schema 是否允许 0。

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

English

Accepted the documentation fix in 4bbe8fd3d. The shared helper now explicitly states that an omitted or zero limit is unbounded and end is the total line count. Kept the existing call-site semantics; did not change zero into an empty result.

Automated follow-up by OpenAI Codex.

中文

接受文档建议,已在 4bbe8fd3d 明确说明:缺省或零 limit 表示不限制,end 为总行数。保留原调用点语义,没有把零改成返回空结果。

OpenAI Codex 自动跟进回复。

const inflight = pendingAcquires.get(sessionId);
if (inflight) return inflight;
const epoch = 0;
// Register before resolveEndpoint, which may synchronously release the session

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 — apps/desktop/src/main/browser/session.ts:249

"Register before resolveEndpoint, which may synchronously release the session before this attempt can be registered in pendingAcquires" — the old code handled that too: epoch stays undefined, a synchronous release sets it to 1, and 1 !== undefined unwinds. The real motivation is that releaseEpochs kept one never-cleared entry per session.

The change itself is fine — there is no await between releaseEpochs.set and pendingAcquires.set, and not bumping with no acquire in flight is safe since the only reader is an in-flight acquire. Just make the comment describe the map's new lifetime instead of a correctness reason that does not hold.

中文

"Register before resolveEndpoint, which may synchronously release the session before this attempt can be registered in pendingAcquires"——旧代码同样处理得了:epoch 保持 undefined,同步 release 置 1,1 !== undefined 照样 unwind。真实动机是 releaseEpochs 每个会话留一条永不清理的记录。

改动本身没问题——releaseEpochs.setpendingAcquires.set 之间没有 await,无在飞 acquire 时不 bump 也安全,因为唯一的读者就是在飞的 acquire。只是把注释改成描述 map 的新生命周期,而不是一个不成立的正确性理由。

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

English

Keeping this comment. Agreed that the old implementation also handled synchronous release, and that the optimization removes historical map entries. The comment is not claiming an old correctness bug: it records an ordering requirement of the new pending-acquire-only representation.

Because release only advances a registered in-flight epoch now, registration must happen before a reentrant resolveEndpoint call, not after it. Describing that required order remains useful even though both implementations are correct. No functional change requested here is needed.

Automated follow-up by OpenAI Codex.

中文

保留这条注释。同意旧实现也能处理同步 release,也同意优化目的是移除历史 map 项。但注释没有声称旧实现存在正确性缺陷,而是在记录新“仅待决 acquire 持有状态”表示的顺序要求。

现在 release 只推进已注册的在飞 epoch,因此必须在可能重入的 resolveEndpoint 调用之前注册,不能放到之后。两版都正确,不影响该顺序说明仍有价值;这里无需功能修改。

OpenAI Codex 自动跟进回复。

test('prewarms once and the shortcut shows and hides synchronously', async () => {
test('creates on first shortcut, then shows and hides synchronously', async () => {
const h = await harness();
await h.controller.refreshSettings();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 — apps/desktop/src/main/__tests__/workhub-presentation.test.ts:555

The rename to creates on first shortcut has nothing asserted between refreshSettings() and the first h.shortcut(). Item 01's only behavioural claim — enabling alone must not create the renderer — has no regression cover; re-adding prewarm leaves this test green.

assert.equal(h.views.length, 0, 'enabling alone must not create the renderer'). Same shape as the assertions at :492 and :508.

中文

改名成 creates on first shortcut,但 refreshSettings() 和第一次 h.shortcut() 之间什么都没断言。第 01 项唯一的行为主张——启用本身不创建渲染器——没有回归保护,把 prewarm 加回来这个测试照样绿。

assert.equal(h.views.length, 0, 'enabling alone must not create the renderer'),和 :492、:508 的断言同形状。

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

English

Accepted in e2721989f. The existing test now asserts views.length === 0 immediately after refreshSettings and before the first shortcut. It also checks that cold loading does not show/focus the window until ready, while subsequent warm toggles remain synchronous. All 24 presentation tests pass.

Automated follow-up by OpenAI Codex.

中文

接受,已在 e2721989f 的现有测试中,在 refreshSettings 后、第一次快捷键前断言 views.length === 0。还检查冷加载期间不显示/聚焦窗口、ready 后展示,以及后续热启动切换仍同步完成。24 项展示控制测试通过。

OpenAI Codex 自动跟进回复。

return new Promise<T>((resolve, reject) => {
const onClose = () => reject(closed.reason);
if (closed.aborted) onClose();
else closed.addEventListener('abort', onClose, { once: true });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 — packages/runtime-host/src/server/session-continuity-coordinator.ts:2270

Good catch on the underlying issue: Promise.race([prepared, connection.closed.then(...)]) attaches a reaction to connection.closed that retains the derived promise's resolving functions, the race result and its fulfillment value — every winning overlay until disconnect.

Two small things. if (closed.aborted) onClose(); else addEventListener(...) falls through after onClose(), running void task.then(...) and a removeEventListener for a listener never registered; onClose(); return; reads better and behaves the same. And #closeConnection aborts with a single Error instance shared by every waiter on that connection, so stacks point at #closeConnection rather than the await site — the old code constructed one per race.

中文

底层问题抓得好:Promise.race([prepared, connection.closed.then(...)])connection.closed 上挂一条 reaction,持有派生 promise 的 resolving functions、race 结果及其 fulfillment value——也就是每个 winning overlay,直到断连。

两点小问题。if (closed.aborted) onClose(); else addEventListener(...)onClose() 后继续往下走,执行 void task.then(...) 和一次针对从未注册的监听器的 removeEventListeneronClose(); return; 更好读且行为相同。另外 #closeConnection 用单个 Error 实例 abort,该连接上所有等待者共享它,栈指向 #closeConnection 而非等待点——旧代码是每次 race 现场新建。

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

English

Not adopting the early return: it is not behavior-equivalent. task is an already-started Promise. Even when closed is already aborted, its rejection must still be observed by task.then.

An A/B run of this actual helper with an already-aborted signal and Promise.reject(new Error('task failed')) produces no unhandledRejection currently; adding the proposed return produces unhandledRejection: task failed. Removing a listener that was never registered is harmless.

Keeping one connection close reason as well. It identifies the shared close cause; the old per-race Error was created in a Promise reaction, not inherently at the caller's await site. This is a diagnostic trade-off, not a demonstrated correctness regression.

Automated follow-up by OpenAI Codex.

中文

不采纳提前 return,因为它并不行为等价。task 是已经启动的 Promise;即使 closed 已经 aborted,仍需通过 task.then 接住任务的拒绝。

对实际 helper 做 A/B:传入已 aborted 的 signal 与 Promise.reject(new Error('task failed')),当前没有 unhandledRejection;加上建议的 return 后出现 unhandledRejection: task failed。移除一个未注册过的 listener 则无害。

同时保留连接共享的关闭原因,用于标识共同的关闭事件。旧版每个 race 的 Error 在 Promise reaction 中创建,并不天然就是调用方 await 位置的堆栈。这属于诊断取舍,没有证实正确性回归。

OpenAI Codex 自动跟进回复。

Fence retired drivers synchronously, then release their snapshots after admitted work and cleanup settle without blocking the next turn.

Generated-by: OpenAI Codex
Handle module loading and client construction inside each bridge's failure boundary so registry status reflects failed connections.

Generated-by: OpenAI Codex
Reuse pending focus to keep the window hidden during initial loading, allow cancellation, and preserve synchronous warm shortcuts.

Generated-by: OpenAI Codex
Initialize composer attachment ownership lazily, avoid empty handshake backing views, clarify line-window and Zod upgrade contracts, and cover CU observer cleanup without restoring allocation probes.

Generated-by: OpenAI Codex
@M4n5ter

M4n5ter commented Sep 11, 2026

Copy link
Copy Markdown
Member Author
English

@Astro-Han — follow-up to the summary review. All 11 inline threads have corresponding per-point replies; the ten summary sections are addressed below.

  1. Collapsed reasoning — explicit trade-off, keeping lazy mounting. Full reasoning bodies require first expansion for accessibility traversal and find-in-page; the keyboard-operable expansion control remains, and subsequent collapse keeps the mounted subtree. Item 22 now spells out both costs. The existing body already disclosed the accessibility-tree change and marked behavior change “Yes.” CONTRIBUTING distinguishes material product decisions from implementation decisions in a PR; it does not impose an issue/Discussion prerequisite for every interaction change. We are not claiming complete behavioral equivalence, and hidden=until-found would retain the heavyweight subtree this item removes.

  2. Replay identity — not adopting the proposed P1 refactor. No supported-input disagreement between the current predicates was demonstrated; the review also says their fields agree today. Routing every messageContentsEqual through two normalization/canonical-JSON/hash operations adds work and allocations to existing comparison callers. Future drift is a maintenance consideration, not evidence of a present replay conflict. Keeping the current implementation.

  3. Shutdown — the two cases have different outcomes.

    • Graph accepted and fixed in ab5266ceb. Retirement fences the old driver synchronously, but the next epoch no longer awaits old teardown I/O. An existing test holds an old operator's stop pending and verifies epoch handover completes; it fails on pre-follow-up source and passes now.
    • WebRTC requested change not adopted. The pinned StreamMuxer::poll_close contract distinguishes remote shutdown notification from drop; it does not promise to drain application substreams. The pinned PC.close implementation sets closing and wakes blocked writers with channel-closed errors, so the previous code was not an ordered application-queue drain either. Ordinary substream drain remains intact. Also, muxer.poll_close already cancels before lifetime.close: moving only the latter cancel cannot fix the proposed issue. No claim is made here of a newly run all-platform queued-data shutdown E2E.
  4. Shared truncation / useLazyRef — fix the concrete problem, not a mandatory abstraction. These callers have different code-point, UTF-16, byte, head/tail, and marker/envelope budgets; the current equivalence checks found no defect. Keeping local bounded implementations. The actual eager attachment lifecycle initialization is fixed in 4bbe8fd3d with a lazy mount initializer, without introducing a generic hook.

  5. Base64 validation/counting — no current bypass established. The existing sites validate first or consume locally encoded input. A hypothetical new caller skipping validation does not make these paths unsafe today. Keeping the validated no-decode counting; no new helper or branded contract is required for this PR.

  6. structuredClone — engine-specific, but measured. The implementation dependency is real; “no measurement” is not. Existing item-35 measurements used the actual Desktop projection/controller path on Chrome 151: eight completed thinking displays retained eight approximately 1.1 MiB parent strings before the change. The compiled clone path retained the same eight 32,768-character displays with none of those oversized parents. This is bounded-display backing retention, not an additive whole-app/RSS number. The premise must be checked on engine upgrades; introducing detachString would not turn it into a language guarantee. ArchiveRead also clones at an object return boundary rather than being the same string-only call.

  7. Deleted tests — not restoring the eight-file batch. The claim that all eight assert only behavior is inaccurate: the file-read/edit-diff files instrument String.prototype.split and assert expandedSlots === 0, and other listed files include GC/WeakRef assertions. Targeted behavior coverage was added/extended in existing Graph, BotRegistry, WorkHub and CU files. No allocation-probe files were restored.

  8. PR-body mapping — two corrections to the review, one clarification applied. Item 07 is implemented by waitForConnectionOpen in session-continuity-coordinator.ts—the same function in the last inline thread. Items 31/32 are implemented in runtime-host/src/server/interactive-run-composer.ts by sharing equal frozen inventories and completed prompt text; they are not the code-point truncation changes. Item 27 now explicitly says locally unknown sessions do not notify observers.

  9. Behavior fixes / commits — already separate and disclosed. Customized cancellation has its own fix commit 5af4f26; CU cleanup is 878520a; replay/admission are c864564 and 34f4ea8, with separate body entries. The Summary covers residency, retention and allocations, and the behavior checkbox is “Yes.” Keeping those accurate classifications rather than treating the entire PR as allocation-only.

  10. Patch exit — documented conditions; upstream reporting remains separate. README already states deletion conditions for all three patches. Missing upstream issue links do not mean there is no exit condition, nor prove that nobody has reported upstream. The Zod upgrade re-verification requirement is now explicit. This task has not filed new upstream issues; that maintenance work is not folded into this scoped fix.

Follow-up changes are pushed through 4bbe8fd. Five affected TypeScript projects compile; 640 tests pass (Graph/Bots 115, WorkHub 24, CU 57, Peer 14, UI 430), plus formatting/whitespace checks. The Graph and SDK regression tests both fail against pre-follow-up source. The full-suite figures already in the PR body are now explicitly labeled as pre-follow-up verification; CI for the new head is running.

Automated follow-up by OpenAI Codex.

中文

对应总评的逐项跟进。11 个行内 thread 均有对应回复;总评十节处理如下。

  1. 折叠 reasoning:明确取舍,保留惰性挂载。 完整正文的无障碍遍历和页内查找需先展开;键盘可操作的展开入口仍在,之后再次折叠保留已挂载子树。第 22 项现已把两项成本都写明。此前正文也已披露无障碍树变化,并勾选行为变化“是”。CONTRIBUTING 区分重大产品决策与可在 PR 中处理的实现级决定,并非所有交互变化都必须先开 issue/Discussion。这里不宣称完全行为等价;hidden=until-found 会保留本项要释放的重型子树。

  2. 重放身份:不采纳所提 P1 重构。 没有证实当前合法输入下的判定差异,review 本身也承认当前字段对齐。让所有 messageContentsEqual 两侧都做规范化、规范 JSON 与哈希,会给既有比较路径增加工作和分配。未来漂移属于维护考虑,不是当前重放冲突的证据,因此保留实现。

  3. 关闭顺序:两处结论不同。

    • Graph 接受并已修复,见 ab5266ceb。旧 driver 仍同步设置关闭围栏,但下一 epoch 不再等待旧 teardown I/O。扩充现有测试,保持旧 operator stop 待决并确认 epoch 交接仍完成;修复前源码失败,当前通过。
    • WebRTC 不采纳所提修改。 固定版本的 StreamMuxer::poll_close 契约区分远端关闭通知与直接 drop,并未保证排空应用子流。固定版本的 PC.close 实现会设置 closing,并让阻塞写得到通道关闭错误,原实现同样不是先有序排空应用队列再关闭。普通子流 drain 保持不变。此外 muxer.poll_close 已在 lifetime.close 前取消,只移动后者的一行并不能修复所假定的问题。这里不冒称新跑了所有平台的带队列关闭 E2E。
  4. 共享截断/useLazyRef:修具体问题,不强制引入抽象。 各调用点的码点、UTF-16、字节、头尾及标记/包络预算不同,现有等价性检查没有发现缺陷,保留局部有界实现。附件 lifecycle 的实际非惰性初始化已在 4bbe8fd3d 改成挂载期惰性初始化,没有新增通用 hook。

  5. Base64 校验/计数:未发现当前绕过路径。 现有入口先校验,或使用本地编码结果。假设未来新调用点漏校验,不能证明当前路径不安全。保留已校验输入的免解码计数,本 PR 不新增 helper 或品牌类型契约。

  6. structuredClone:依赖引擎,但有测量。 实现依赖属实,“没有测量”不属实。第 35 项既有实验使用 Chrome 151 中真实 Desktop projection/controller 路径:修改前八个已完成 thinking 显示保留八个约 1.1 MiB 父字符串;实际编译后的 clone 路径保留相同的八份 32,768 字符显示,但不再保留这些超大父字符串。这是有界显示的底层驻留,不是可相加的整应用/RSS 数值。引擎升级时仍需复核;引入 detachString 也不能将其变成语言保证。ArchiveRead 还是对象返回边界的 clone,不是完全相同的字符串调用。

  7. 删除的测试:不整批恢复八个文件。 “八个全是行为断言而非分配”并不准确:file-read/edit-diff 会插桩 String.prototype.split 并断言 expandedSlots === 0,其他列出的文件也包含 GC/WeakRef 断言。本轮在现有 Graph、BotRegistry、WorkHub、CU 文件中补充关键行为覆盖,没有恢复分配量探针文件。

  8. 正文对应关系:两处纠正 review,一处已澄清。 第 07 项由 session-continuity-coordinator.ts 的 waitForConnectionOpen 实现,也就是最后一条行内评论引用的函数。31/32 在 runtime-host/src/server/interactive-run-composer.ts 共享相同冻结 inventory 与已完成 prompt 正文,不是码点截断。第 27 项现已明确本地未知会话不通知 observer。

  9. 行为修复/独立提交:此前已有且已披露。 请求取消有独立 fix 提交 5af4f26;CU 清理为 878520a;重放/接纳分别为 c864564、34f4ea848,各有正文条目。概要覆盖驻留、保留与分配,行为变化勾选“是”。保留这些真实分类,不将整个 PR 错归为纯分配优化。

  10. 补丁退出:已有删除条件,上游报告另行处理。 README 已为三个补丁写出删除条件。缺上游 issue 链接不等于没有退出条件,也不能证明无人上报。Zod 升级前的复核要求现已补明。本任务没有新开上游 issue,不把该维护工作扩进本轮限定修复。

后续修复已推送至 4bbe8fd。五个受影响 TypeScript 项目编译通过,640 项测试通过(Graph/Bot 115、WorkHub 24、CU 57、Peer 14、UI 430),格式及空白检查通过。Graph 与 SDK 回归测试在修复前源码上均确认失败。PR 原有全套验证数字现已明确标注为后续修复前的结果,新 HEAD 的 CI 正在运行。

OpenAI Codex 自动跟进回复。

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed at b2c6333f4, fresh, against the previous round. Five follow-up commits land the two P1 inline findings and four of the smaller ones, each with a regression test that I confirmed fails on the pre-follow-up source. Of the seven declined items I checked on the code rather than the argument: five rebuttals are correct and two earlier findings were overstated — the replay-identity P1 and the WebRTC half of the shutdown P2 do not describe a present defect, and I withdraw them. One new issue falls out of the cold-summon fix itself and is filed inline.

Status of the previous round

# Finding Status Where
body 1 Collapsed reasoning bodies leave the DOM Declined, partly correct see P2 below
body 2 Replay identity has two authorities Declined, rebuttal accepted — withdrawn verified field-for-field, below
body 3a #retireDriver blocks the next epoch Fixed ab5266ceb, stream-graph-coordinator.ts:1437,1444
body 3b WebRTC cancel-before-close Declined, rebuttal accepted — withdrawn muxer.rs:148 already cancels first
body 4 One invariant rederived 13 times Declined acceptable; no defect shown at any site
body 5 base64 validate/measure separable Declined all four sites validate first — verified
body 6 structuredClone premise Declined with a measurement acceptable
body 7 Behaviour tests deleted in 44bc559f6 Partly addressed 4 of 8 replaced; see P2 below
body 8 Body entries do not match the diff Fixed items 07/27/31/32 corrected; item 07 is waitForConnectionOpen — author is right
body 9 Behaviour fixes filed as allocation work Declined commits were already separate; accepted
body 10 Patches have no upstream exit Partly addressed zod re-verification note added to patches/README.md:56
inline 1 require outside the failure boundary Fixed 4aa979987; bot-registry.test.ts:29 is red on base
inline 2 Cold WorkHub summon has no warm-up Fixed e2721989f; 2 tests red on base. See P2 below
inline 3 known gate needs a regression test Fixed maka-cu-backend.test.ts:1113; red when the gate is removed
inline 4 WeChat SSE rewrite unmeasured Declined with a measurement acceptable
inline 5 Non-lazy useRef lifecycle Fixed use-composer-attachments.ts:214
inline 6 zod previousHandoff Declined, counterexample is valid the Proxy length getter reenters before alloc
inline 7 chunk clears are no-ops Declined with a measurement correct — block scope says nothing about a suspended frame's registers
inline 7b zero-length handshake remainder Fixed peer-native.ts:413
inline 8 limit === 0 undocumented Fixed text-line-window.ts:22
inline 9 browser/session.ts comment Declined fine
inline 10 missing views.length === 0 assertion Fixed workhub-presentation.test.ts:559
inline 11 onClose(); return; Declined, rebuttal correct the early return orphans task's rejection

On body 2: messageContentsEqual and messageContentDigest do agree on every supported input. AttachmentRef, DirectoryReference, QuoteRef and InlineReference are closed shapes declared through defineObjectShape, and every field of each is compared by the old predicate and kept by normalizeMessageContent; the empty-array-vs-undefined collapse matches on attachments, quotes and directoryReferences, and is deliberately absent on inlineReferences in both. No input separates them, so this is a maintenance preference, not a replay defect. The same applies to the digest change in root-admission-owner.ts.

On body 3b: muxer.rs:148 calls lifetime.cancellation.cancel() before lifetime.close(), so moving the cancel inside close() changes nothing on the production path. The residual delta — workers aborted before peer_connection.close(), where on main there was no cancellation at all — costs at most the tail of an outgoing substream at connection teardown, which resumable-peer-stream retransmits. P3, not blocking.

P2 — Never-expanded reasoning is unreachable to find-in-page and screen readers

Reachability class ① — open a restored transcript and search it.

packages/ui/src/astryx-chat-reasoning.tsx:149 renders {isExpanded || hasExpanded ? children : null}. The previous collapse was grid-template-rows: 0fr plus overflow: hidden, which keeps the subtree in the accessibility tree and findable with Cmd+F. A reasoning block that was never opened in this session — which is every block in a transcript restored from history, exactly where searching matters — now has no text to find.

The process half of the earlier finding does not hold, and I withdraw it: CONTRIBUTING.md:30 reserves the dev@ list for "project direction, governance, and material product decisions" and says implementation-level decisions may live in the pull request. This is an implementation decision.

The accessibility cost stands on its own and is not mitigated by the header staying keyboard-operable — a reader has to know the text is there to expand it. hidden="until-found" restores find-in-page but not the memory, so it is not a free fix. What would settle it is a number: item 22 claims a DOM-residency win with no measurement, and the size of that win is what decides whether the trade is worth making. Either attach one, or scope the deferral to blocks above some length so short reasoning stays searchable.

P2 — Four of the eight behaviour tests are still missing, and they cover the least-verified changes

Reachability class ② — the changes they would cover are cancellation, shutdown and recovery paths.

44bc559f6 remains as pushed. The follow-up added targeted coverage in four existing suites — Graph retirement, BotRegistry SDK failure, WorkHub cold summon, CU session cleanup — and I confirmed all four fail on the pre-follow-up source. The author is right that the file-read and edit-diff files were allocation probes and that some of the others carried GC/WeakRef assertions.

What is left uncovered is the part with the least independent evidence:

  • 5af4f263b restores cancellation through request customization (request-customization-fetch.ts:51). This is a behaviour fix on an abort path with no test that fails without it.
  • 1789afeac gates the Goal token cache on the control lease (goal-coordinator.ts:165) and clears it on removal.
  • The zod patch's open/handoff handling, whose only evidence is the counterexample script in the inline thread.
  • lifetime.rs cancellation ordering, whose Rust lifecycle test was removed.

Each is one test in an existing suite, in the same shape as the four that landed. Cherry-picking from 44bc559f6^ is cheaper than rewriting, and the GC assertions can be dropped from the restored files.

Verification

Clean npm ci, all affected workspaces built; runtime 98, runtime-host 108, computer-use 117, ui 430, desktop main 2468 green; lint and format clean. Red-on-base (production file reverted alone): graph epoch handover, BotRegistry SDK failure, both WorkHub cold-summon tests, CU known gate — all five fail before the follow-up. Also re-derived with no defect found: the four Buffer.byteLength(x, 'base64') sites, sliceLineByBytes / truncateToolOutput at degenerate budgets, a 120 000-case base-vs-head differential fuzz of createEditUnifiedDiff / createUnifiedDiff (zero diffs), the peer-mesh/node.ts wake rewrite, the app-shell.tsx hydration fence, and the WeChat iterator completion protocol.

Nothing else at P0–P2. With the three above addressed — or, for the first two, answered with a measurement — this is ready to approve.

中文

b2c6333f4 上重新评审。五个跟进 commit 落实了两条 P1 行内意见和四条小意见,每条都带回归测试;我逐一在跟进前的源码上确认它们会失败。七条被拒的意见我按代码而不是按论据核对:五条反驳成立,两条原意见站不住——重放身份那条 P1 和关闭顺序 P2 的 WebRTC 一半都不构成现存缺陷,我撤回。冷启动那条修复本身引出一个新问题,已作为行内评论提出。

上一轮意见的处理状态

# 意见 状态 位置
正文 1 折叠 reasoning 正文离开 DOM 拒绝,部分成立 见下方 P2
正文 2 重放身份有两个权威 拒绝,反驳成立——撤回 已逐字段核对,见下
正文 3a #retireDriver 阻塞下一个 epoch 已修 ab5266cebstream-graph-coordinator.ts:1437,1444
正文 3b WebRTC 先 cancel 后 close 拒绝,反驳成立——撤回 muxer.rs:148 本就先 cancel
正文 4 同一不变量推导 13 遍 拒绝 可接受;任何调用点都没证明出缺陷
正文 5 base64 校验与测长可分离 拒绝 四个调用点都先校验——已核实
正文 6 structuredClone 前提 拒绝并给出测量 可接受
正文 7 44bc559f6 删掉行为测试 部分处理 8 个补回 4 个;见下方 P2
正文 8 正文条目与 diff 不符 已修 07/27/31/32 已更正;07 确为 waitForConnectionOpen,作者是对的
正文 9 行为修复归在分配优化下 拒绝 commit 本就是分开的;接受
正文 10 补丁没有上游出口 部分处理 patches/README.md:56 补了 zod 升级复核要求
行内 1 require 在失败边界外 已修 4aa979987bot-registry.test.ts:29 在基线上会红
行内 2 冷启动快捷键没有预热 已修 e2721989f;两个测试在基线上会红。见下方 P2
行内 3 known 门需要回归测试 已修 maka-cu-backend.test.ts:1113;抽掉那个门就会红
行内 4 微信 SSE 重写没有测量 拒绝并给出测量 可接受
行内 5 useRef 非惰性 已修 use-composer-attachments.ts:214
行内 6 zod previousHandoff 拒绝,反例成立 数组 Proxy 的 length getter 在 alloc 前重入
行内 7 chunk 清空是 no-op 拒绝并给出测量 正确——块作用域说明不了挂起帧的寄存器
行内 7b 零长度握手 remainder 已修 peer-native.ts:413
行内 8 limit === 0 未写明 已修 text-line-window.ts:22
行内 9 browser/session.ts 注释 拒绝 可以
行内 10 views.length === 0 断言 已修 workhub-presentation.test.ts:559
行内 11 onClose(); return; 拒绝,反驳正确 提前 return 会让 task 的拒绝无人接管

关于正文 2:messageContentsEqualmessageContentDigest 在所有受支持输入上确实一致。AttachmentRefDirectoryReferenceQuoteRefInlineReference 都是经 defineObjectShape 声明的封闭形状,每个字段旧判据都比较、normalizeMessageContent 都保留;空数组与 undefined 的归一在 attachmentsquotesdirectoryReferences 上两边一致,在 inlineReferences 上两边同样刻意不做。没有输入能把两者分开,所以这是维护偏好而不是重放缺陷。root-admission-owner.ts 的摘要改动同理。

关于正文 3b:muxer.rs:148lifetime.close() 之前就调用了 lifetime.cancellation.cancel(),把 cancel 挪到 close() 里面对生产路径毫无影响。残留差异——worker 在 peer_connection.close() 之前被取消,而 main 上根本没有取消机制——最多损失连接拆除时某个子流的尾部字节,resumable-peer-stream 会重传。P3,不阻塞。

P2 — 从未展开的 reasoning 对页内查找和屏幕阅读器不可达

可达类别 ①——打开一份恢复的会话记录并搜索它。

packages/ui/src/astryx-chat-reasoning.tsx:149 渲染 {isExpanded || hasExpanded ? children : null}。原来的折叠是 grid-template-rows: 0froverflow: hidden,内容留在可访问性树里、能被 Cmd+F 命中。本次会话里从未被打开过的 reasoning 块——也就是从历史恢复的记录里的每一个块,恰恰是最需要搜索的地方——现在没有任何文本可供查找。

原意见中的流程论据不成立,我撤回:CONTRIBUTING.md:30 把 dev@ 邮件列表限定为"项目方向、治理与实质产品决策",并写明实现层面的决策可以留在 PR 里。这属于实现决策。

可访问性代价本身成立,"header 仍可键盘操作"并不能抵消——读者得先知道文本在那里才会去展开。hidden="until-found" 能恢复页内查找但保不住内存,所以不是免费的修法。真正能定案的是一个数字:第 22 项声称的 DOM 驻留收益没有测量,而这个收益的大小决定了这笔交换值不值。要么补上测量,要么把延迟挂载限定在超过某个长度的块上,让短 reasoning 保持可搜索。

P2 — 八个行为测试还缺四个,而且缺的正是独立证据最少的部分

可达类别 ②——它们要覆盖的是取消、关闭与恢复路径。

44bc559f6 保持原样。跟进在四个既有套件里补了针对性覆盖——Graph 退役、BotRegistry SDK 失败、WorkHub 冷启动、CU 会话清理——我确认这四个在跟进前的源码上都会失败。作者说得对:file-read 与 edit-diff 那两个文件是分配量探针,其余几个里确实有 GC/WeakRef 断言。

剩下没覆盖的,正是独立证据最少的部分:

  • 5af4f263b 恢复了请求定制路径上的取消(request-customization-fetch.ts:51)。这是 abort 路径上的行为修复,没有任何测试会因为缺它而失败。
  • 1789afeac 把 Goal token 缓存挂到 control lease 上(goal-coordinator.ts:165),并在移除时清理。
  • zod 补丁的 openhandoff 处理,目前唯一证据是行内回复里的反例脚本。
  • lifetime.rs 的取消顺序,对应的 Rust 生命周期测试已被删除。

每一项都是在既有套件里加一个测试,形状和已落地的那四个一样。从 44bc559f6^ 挑回来比重写便宜,恢复的文件里去掉 GC 断言即可。

验证

干净 npm ci,受影响 workspace 全部构建;runtime 98、runtime-host 108、computer-use 117、ui 430、desktop main 2468 全绿;lint、format 干净。基线红检(只回退生产文件):graph epoch 交接、BotRegistry SDK 失败、两个 WorkHub 冷启动测试、CU known 门——五项在跟进前全部失败。另外独立推导未发现缺陷:四个 Buffer.byteLength(x, 'base64') 调用点、sliceLineByBytes / truncateToolOutput 的退化预算、createEditUnifiedDiff / createUnifiedDiff 基线对 head 的 120 000 组差分 fuzz(零差异)、peer-mesh/node.ts 唤醒重写、app-shell.tsx 水合 fence、微信迭代器完成协议。

P0–P2 没有其他问题。上面三条处理掉——前两条也可以用一次测量来回答——就可以 approve。

if (!view || view.webContents.isDestroyed() || !rendererReady || !parent || parent.isDestroyed()) return;
// A cold summon stays hidden until the renderer has mounted its composer.
// Reuse focusPending so hide/disable can cancel it before ready arrives.
if (placement === 'floating' && progressRequest === undefined) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A cold summon now keeps the floating window hidden for the whole renderer bootstrap, and ensureView() never clears backgroundThrottling — so the bootstrap this item is trying to speed up runs in a backgrounded, timer-throttled renderer. requestProgress() at :365 already handles the same "paint while hidden, show on acknowledgement" shape with setBackgroundThrottling(false), restored by clearProgressRequest() at :304. Do the same here: disable it when the cold summon starts and re-enable it in the ready branch at :476. (Not a deadlock — ready comes from a plain mount effect, not an animation frame.)

中文

冷启动现在让浮窗在整个渲染器启动期间保持隐藏,而 ensureView() 从不清除 backgroundThrottling——于是本项想加速的那段启动过程跑在一个被后台化、定时器被节流的渲染器里。requestProgress() 在 :365 已经用 setBackgroundThrottling(false) 处理了同样的「隐藏中绘制、收到确认再显示」形状,并由 :304 的 clearProgressRequest() 恢复。这里照做:冷启动开始时关掉,在 :476 的 ready 分支恢复。(不会死锁——ready 来自普通挂载 effect,不经动画帧。)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

English

Keeping the current implementation: the new hidden window is not initially a backgrounded document. Electron's page-visibility contract gives a show:false window initial visible state; this path does not set paintWhenInitiallyHidden:false.

I checked the actual Desktop bootstrap on Linux/xvfb, Electron 43.4.1 / Chromium 150 / Node 24.18.1, with fresh profiles. The final A/B recorded visibility in an init script and at DOMContentLoaded, before first show:

Observation Default throttling Explicitly disabled
Native window visible at DOMContentLoaded false false
Initial / DOMContentLoaded document visibility visible / visible visible / visible
WebContents creation → first show 337.89 ms 333.84 ms

The probe bypassed Playwright's Electron loader, asserted that disable-background-timer-throttling was absent, and disabled focus emulation. This is path verification, not cross-platform latency statistics.

The progress path reuses a previously hidden container and waits for two animation frames; normal cold readiness comes from a mount effect. They do not establish the same background premise. I therefore am not adding a throttle toggle for an unproven cold-start delay. An unconditional reset in normal ready could also interfere with a pending progress preparation.

中文

维持当前实现:新建隐藏窗口不等于文档已进入后台。Electron 的可见性契约规定 show:false 窗口的初始文档状态仍为 visible;此路径没有设置 paintWhenInitiallyHidden:false

真实 Desktop bootstrap,Linux/xvfb、Electron 43.4.1/Chromium 150/Node 24.18.1、独立 profile:在 init script 和 DOMContentLoaded 中提前记录可见性,两组在首次显示前原生窗口均为不可见,文档初始及 DOMContentLoaded 状态均为 visible。默认节流/主动关闭节流的 WebContents 创建至首次 show 分别为 337.89/333.84 ms。探针绕过 Playwright Electron loader,确认没有 disable-background-timer-throttling,并关闭 focus emulation;这不是跨平台延迟统计。

进度路径复用已隐藏过的容器、等待两次动画帧,普通冷启动 ready 来自 mount effect,不能直接套用相同后台前提。因此不为尚未证实的冷启动延迟新增节流切换;普通 ready 中无条件恢复也可能干扰尚未完成的进度准备。

@M4n5ter

M4n5ter commented Sep 11, 2026

Copy link
Copy Markdown
Member Author
English

Follow-up to the second review, pushed as 2bb009541. No production implementation changes in this follow-up.

Coverage: accepted the four narrow contract gaps, without restoring the deleted allocation/stress suites.

  • Request customization: inherited/overridden/null signals, plus real HTTP response cancellation after temporary Requests are collected. Reverting to the temporary Request's signal makes the HTTP assertion time out instead of reporting AbortError. GC exposes the native weak-forwarding failure; there is no heap-size assertion.
  • Goal: an old context read completes during a new Goal's evaluation after archive/unarchive; the new Goal must establish a 120-token baseline. Removing the lease gate produces 30 instead. The existing retirement test also checks rollback preserves the projection and durable authority. No global Map interception.
  • Zod: a small Runtime contract suite covers ESM/CJS cycles, shared aliases, reentry before allocation, and thrown-parse recovery. Replacing handoff restoration with clearing breaks root-cycle identity in both entry points. The patch-upgrade note links this suite.
  • Native WebRTC: the existing suite now checks both explicit close and drop release a backpressured writer while its application reader remains alive. This asserts the teardown outcome, not an internal cancel/close order; it is not a comprehensive native resource-leak test.

Two affected TypeScript workspaces build; 18 targeted JS tests and all 40 Rust tests pass. Lint/format, ASF headers and Rust Clippy pass. The signal/lease/handoff fault-injection failures above were checked independently; no new full Desktop or cross-platform run is claimed.

Reasoning: measurement and cost are now in item 22's accompanying PR-body section. Real TurnView, separate Chromium 151 processes, fixed synthetic input: 20 reasoning blocks × 180 code lines, 20 collapsed tool results, one final answer; identical 376,472-byte viewmodel JSON.

Metric Before Current
Post-GC JS heap 10,839,440 B 4,895,284 B
DOM elements 5,020 679

The 5.67 MiB difference is component residency, not OS RSS or a typical-workload claim. The fresh page-search probe does find the marker before expansion in the old version, but only after expansion now. My earlier “not found in either version” probe was not reproducible and is not a rebuttal. Keyboard-operable headers do not replace discoverability; real screen readers were not tested. I retain first-expansion mounting with that disclosed trade-off, without inventing a length threshold.

WorkHub: answered in the new inline thread with initial visibility and cold-summon A/B evidence; no throttle toggle added. The previously accepted/withdrawn points stay closed to this adjudication.

中文

针对第二轮 review的跟进已推送:2bb009541。本轮没有修改生产实现。

**测试覆盖:**采纳四个窄契约覆盖点,不恢复已删除的分配量/压力测试套件。

  • 请求定制:覆盖继承/覆盖/null signal,以及临时 Request 被回收后真实 HTTP 响应仍能取消。改回临时 Request 的 signal 后,断言由 AbortError 变为超时。GC 用于暴露原生弱转发故障,没有堆大小断言。
  • Goal:归档/解归档后,旧 context read 在新 Goal 评估期间完成,新基线仍须为 120;移除 lease 检查后实际变成 30。既有 retirement 测试也验证 rollback 保留投影和持久 authority。不拦截全局 Map。
  • Zod:在 Runtime 增加小型 ESM/CJS 契约套件,覆盖循环、共享别名、分配前重入和异常后恢复。将 handoff 恢复改为清空,两种入口均破坏根循环身份。补丁升级说明指向此套件。
  • Native WebRTC:扩展既有测试,验证显式关闭/直接 drop 都能释放背压写入,应用层 reader 此时仍存活。断言拆除结果,不锁内部取消顺序,也不冒称完整原生资源泄漏测试。

两个受影响 TypeScript 工作区编译通过,18 项定向 JS 测试、Rust 全套 40 项、lint/格式、ASF 文件头及 Rust Clippy 通过。上述三种故障注入已分别确认失败;本轮没有重跑完整 Desktop 或跨平台验证。

**Reasoning:**测量条件与代价已补入 PR 正文第 22 项的配套说明。真实 TurnView、独立 Chromium 151 进程、固定合成样本:20 段 reasoning × 180 行代码、20 个折叠工具结果、一个最终答复,输入 viewmodel JSON 均为 376,472 字节。GC 后 JS heap 从 10,839,440 降至 4,895,284 字节,约少 5.67 MiB;DOM 元素从 5,020 降至 679。这不是 OS RSS 或典型使用分布。

本轮页内查找复验确认:旧版展开前能找到深层标记,新版须先展开。此前“两版都找不到”的探针未能复现,不再用作反驳。键盘可展开不能替代内容可发现性;未测试真实屏幕阅读器。继续维持首次展开挂载,披露这一取舍,不凭空增加长度阈值。

**WorkHub:**已在新增行内线程给出初始可见性及冷召唤 A/B 证据,不新增节流切换。已接受/撤回的旧项不重新开启裁决。

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Third round on 2bb009541. Two of the three items are settled; the new Rust test does not pass on macOS.

1. Reasoning DOM residency — accepted. The measurement is there and the method is sound for what it claims: identical 376,472-byte viewmodel, real TurnView, separate renderer processes, post-GC heap and DOM element counts (10,839,440 → 4,895,284 B; 5,020 → 679 elements), explicitly scoped to component residency rather than RSS or a typical workload. Retracting the earlier "not found in either version" probe and stating plainly that find-in-page now requires expansion is the right call, and it makes the trade-off reviewable instead of disputed. hidden="until-found" is correctly rejected: it only works on a mounted subtree, which is exactly the residency this change removes, and the collapsed panel is display-hidden, so screen readers never reached that content in either version either. No length threshold needed.

2. Restored coverage — three of four confirmed, lifetime.rs not. Verified by reverting each production change and rebuilding: request-customization-fetch.ts:51 (both new cases red — the signal assertion, and the HTTP one timing out at ~2.1 s as described), goal-coordinator.ts:165 (baseline 30 instead of 120, exactly as claimed), and the zod handoff restore (both ESM and CJS red; removing the try/finally alone also turns ESM red, so throw-recovery is covered too). connection_teardown_releases_a_backpressured_writer fails on macOS 26.6 arm64, 3/3 runs, at tests.rs:144 — the 4 MiB write_all returns Ok(Ok(())) inside 100 ms, so the precondition that the writer is backpressured never holds and the teardown contract is never exercised. CI only runs cargo test on ubuntu-24.04 (runtime-host-peer-admission.yml:56), so it stays green there while failing for every macOS contributor. Fix: instead of asserting one fixed-size write stalls, write bounded chunks in a loop until a write actually fails to complete within the window (or drive the write in a task and assert only the release), so the test states the contract without depending on a host's SCTP buffer size.

3. WorkHub background throttling — accepted, rejection is correct. Electron's Page visibility section says a window created with show: false gets initial visibility state visible despite being hidden, and this path does not set paintWhenInitiallyHidden: false, so the cold-summon bootstrap is not a backgrounded document. The code confirms the asymmetry with requestProgress independently: the progress card signals readiness from a double requestAnimationFrame (workhub-progress-card.tsx:57), which a suspended RAF would deadlock — that is why #5114 disabled throttling at :365 — whereas cold-summon readiness comes from a mount effect (workhub-surface-switch.tsx:33), which throttling cannot stall. The 337.89 vs 333.84 ms A/B is consistent with that and no toggle is warranted.

中文

针对 2bb009541 的第三轮。三项中两项已了结;新增的 Rust 测试在 macOS 上不通过。

1. Reasoning DOM 驻留 —— 接受。 测量已给出,方法与其声称的范围相符:同一份 376,472 字节 viewmodel、真实 TurnView、独立渲染进程、GC 后堆与 DOM 元素数(10,839,440 → 4,895,284 字节;5,020 → 679 个元素),并明确限定为组件驻留,而非 RSS 或典型负载。撤回此前“两版都找不到”的探针、直说现在页内查找必须先展开,是正确的处理,使这个取舍变成可评审的事实而非争议。hidden="until-found" 被否掉也是对的:它只对已挂载的子树有效,而这正是本次改动要去掉的驻留;且折叠面板本身是隐藏显示的,两个版本里屏幕阅读器都读不到该内容。不需要长度阈值。

2. 恢复的覆盖 —— 四项中确认三项,lifetime.rs 未确认。 逐个回退生产改动并重新构建验证:request-customization-fetch.ts:51(两个新用例均转红——signal 断言,以及 HTTP 用例如其所述在约 2.1 秒超时)、goal-coordinator.ts:165(基线变成 30 而非 120,与其所称完全一致)、zod 的 handoff 恢复(ESM 与 CJS 均转红;单独去掉 try/finally 也会让 ESM 转红,说明抛错恢复同样被覆盖)。connection_teardown_releases_a_backpressured_writer 在 macOS 26.6 arm64 上 3/3 次失败于 tests.rs:144——4 MiB 的 write_all 在 100 ms 内返回 Ok(Ok(())),写入被背压的前提根本不成立,拆除契约也就从未被执行。CI 只在 ubuntu-24.04 上跑 cargo testruntime-host-peer-admission.yml:56),所以 CI 绿而每个 macOS 贡献者本地都红。修法:不要断言某个固定大小的写入会停住,而是循环写入有界分块直到某次写入确实在窗口内未完成(或把写入放进任务、只断言释放),让测试表达契约而不依赖某台主机的 SCTP 缓冲大小。

3. WorkHub 后台节流 —— 接受,拒绝是正确的。 Electron 的 Page visibility 一节写明,以 show: false 创建的窗口初始可见性状态为 visible,尽管窗口实际隐藏;此路径也没有设置 paintWhenInitiallyHidden: false,因此冷召唤的 bootstrap 不是后台文档。代码本身独立佐证了与 requestProgress 的不对称:进度卡片的就绪信号来自双重 requestAnimationFrameworkhub-progress-card.tsx:57),RAF 被挂起就会死锁——这正是 #5114 在 :365 关闭节流的原因;而冷召唤的就绪来自 mount effect(workhub-surface-switch.tsx:33),节流无法使其停滞。337.89 对 333.84 ms 的 A/B 与此一致,不需要新增开关。

@M4n5ter

M4n5ter commented Sep 11, 2026

Copy link
Copy Markdown
Member Author
English

Accepted the test-portability finding from round three. Fixed in 13f27da75; only webrtc_direct/tests.rs changed.

The test now writes and flushes reusable 8 KiB chunks until an individual operation remains pending for 100 ms, instead of requiring a fixed 4 MiB write to stall. Setup has a 5-second deadline and a 64 MiB traffic guard. The timed-out operation is retained—not cancelled or restarted—through explicit close or drop, with the application reader still alive.

The pending operation must settle within 5 seconds. Already-queued bytes may complete successfully during close, so the test does not incorrectly require that operation to fail; it separately asserts that a new write/flush after teardown fails. Each chunk is flushed to keep libp2p framing within the transport message limit.

Validation on Linux x86_64: the targeted case passed 10 consecutive reruns, the full Rust suite passed 40/40, and Clippy/all-targets plus formatting passed. No production cancellation order or buffer configuration changed. I cannot independently verify macOS here; please rerun this revision on the macOS machine that exposed the failure.

中文

采纳第三轮指出的测试可移植性问题,已在 13f27da75 修正,仅修改 webrtc_direct/tests.rs

测试改为复用 8 KiB 分块逐次写入并 flush,直到某次操作确实持续待决 100 ms,不再要求固定 4 MiB 写入必然阻塞。准备阶段限制为 5 秒、最多 64 MiB 流量;保留超时的同一个 future 穿过显式 close/drop,应用层 reader 始终存活。

待决操作必须在 5 秒内退出。已经排队的字节可能在关闭期间成功完成,因此不再错误地要求该操作必定失败,而是另外断言关闭后的新 write/flush 必须失败。逐块 flush 也使 libp2p 帧保持在传输消息大小限制内。

Linux x86_64 验证:定向用例连续重跑 10 次通过,Rust 全套 40/40、Clippy/all-targets 和格式检查通过。未修改生产取消顺序或缓冲配置。本机无法独立验证 macOS,请在此前复现失败的 macOS 机器上复跑此版本。

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fourth round on 13f27da75. The one open item is fixed: git diff 2bb009541..13f27da75 --stat is webrtc_direct/tests.rs only, +52/-25, no production change. The test now states the contract instead of a host's buffer size, and it passes here.

Item (round three) State
1. Reasoning DOM residency settled (accepted round three)
2. Restored coverage — request-customization-fetch, goal-coordinator, zod handoff settled (verified round three)
2. connection_teardown_releases_a_backpressured_writer portability fixed in 13f27da75
3. WorkHub background throttling settled (accepted round three)

Backpressure is now observed, not assumed. The loop at native/runtime-host-peer/src/webrtc_direct/tests.rs:146 writes and flushes 8 KiB chunks until one operation is still pending after 100 ms, and carries that future through teardown — it is not a bigger fixed write or a longer sleep. Instrumenting the loop on this machine shows it needs 6,643,712 bytes before a write stays pending, identically for both explicit_close arms, which is exactly why the previous fixed 4 MiB write_all returned Ok inside the window here.

It cannot hang. Both guards are checked before each attempt: sent < 64 MiB (:147) and a 5-second setup_deadline (:151), so a host that never backpressures fails with a named panic rather than spinning. Every await after that is inside a 5-second timeout.

Dropping the pending write's error assertion is the right call. Bytes already queued can legitimately complete during close, so let _ = ...timeout(..., &mut writing) (:180) keeps the part that is actually contractual — the operation must settle, not stay parked — and the observable claim moves to the post-teardown write at :184, which must fail. That is a stronger statement than the old one and does not encode a close/cancel ordering.

Verified on macOS 26.6 arm64 (cargo 1.98.0): cargo test --locked --lib webrtc_direct passed 5/5 consecutive runs (6 tests each), and cargo test --locked for the crate passed 39/39. The probe edit above was reverted before those runs; the tree matches 13f27da75. No P0–P2 findings, and nothing at P3.

Nothing outstanding; approving.

中文

针对 13f27da75 的第四轮。唯一未了结项已修复:git diff 2bb009541..13f27da75 --stat 只动了 webrtc_direct/tests.rs,+52/-25,没有生产改动。测试现在表达的是契约,而不是某台主机的缓冲大小,并且在本机通过。

第三轮条目 状态
1. Reasoning DOM 驻留 已了结(第三轮接受)
2. 恢复的覆盖 —— request-customization-fetchgoal-coordinator、zod handoff 已了结(第三轮已验证)
2. connection_teardown_releases_a_backpressured_writer 可移植性 已在 13f27da75 修复
3. WorkHub 后台节流 已了结(第三轮接受)

背压现在是被观察到的,不是假定的。 native/runtime-host-peer/src/webrtc_direct/tests.rs:146 的循环逐次写入并 flush 8 KiB 分块,直到某次操作在 100 ms 后仍然待决,并把同一个 future 带过拆除——不是更大的固定写入,也不是更长的 sleep。在本机给循环加探针可见:需要 6,643,712 字节才会出现真正待决的写入,两个 explicit_close 分支完全一致;这正是此前固定 4 MiB 的 write_all 在本机窗口内返回 Ok 的原因。

它不会挂住。 两个保护在每次尝试前都会检查:sent < 64 MiB(:147)和 5 秒的 setup_deadline(:151),因此永不背压的主机会以具名 panic 失败,而不是空转。之后每个 await 都包在 5 秒 timeout 里。

去掉对待决写入的错误断言是对的。 已排队的字节在关闭期间合法地完成是可能的,因此 let _ = ...timeout(..., &mut writing)(:180)保留了真正属于契约的部分——该操作必须退出,而不是一直卡住——可观察的断言则移到 :184 拆除后的写入,那一次必须失败。这比原来的说法更强,也没有锁死 close/cancel 的顺序。

在 macOS 26.6 arm64(cargo 1.98.0)验证:cargo test --locked --lib webrtc_direct 连续 5 次全部通过(每次 6 项),整个 crate 的 cargo test --locked 通过 39/39。上述探针改动已在这些运行前回退,工作树与 13f27da75 一致。没有 P0–P2 问题,P3 也没有。

没有遗留问题;批准。

Resolve the archive E2E race with main's authoritative busy check and synchronize the merged renderer debt count.

Generated-by: OpenAI Codex
@Astro-Han

Copy link
Copy Markdown
Contributor

Approved above. CI is still running on 13f27da75; please merge once it is green — squash title perf: incremental memory optimization across Maka (#5153).

Revalidated the byte-count-only protocol changes against current main after merging its Skill query epoch bump.

Generated-by: OpenAI Codex
@M4n5ter
M4n5ter merged commit 0b7bf39 into main Sep 11, 2026
17 checks passed
@M4n5ter
M4n5ter deleted the perf/desktop-idle-memory branch September 11, 2026 11:08
Astro-Han added a commit that referenced this pull request Sep 11, 2026
Only `apps/desktop/src/main/workhub-presentation.ts` conflicted. This branch
routed WorkHub's summon through `focusWindow(target, deps.revealMode)` at the
end of `detach()`; main's #5153 made the WorkHub renderer lazy and moved that
reveal out of `detach()` into `focusComposer()`, where a cold summon now waits
for the renderer to mount its composer before the native window appears.

Resolved by keeping main's placement and this branch's authority: the reveal
that `focusComposer()` performs is `focusWindow(parent, deps.revealMode)`
instead of the inline `isMinimized()/restore()/show()/focus()`, and `detach()`
no longer reveals at all. Every remaining reveal in the file goes through the
gate — `focusWindow` in `focusComposer()` and `navigateMain()`,
`showWindowInactive` for the progress card — and the lazy-creation path adds no
ungated `show()`/`focus()`. Under `hidden` the window stays hidden and the
`!parent.isVisible()` early return keeps `focusPending` armed, which is what
main's cancellation tests already assert.

`workhub-presentation.test.ts` auto-merged but needed one semantic fix: the
reveal-mode test asserted on the window immediately after `show()`, which no
longer reveals anything. It now asserts nothing is revealed before `ready` and
the mode-specific reveal after it.

Generated-by: Claude Code
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/XL Under 2500 readable lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants